# -*- coding: utf-8 -*-
################################ Begin license #################################
# Copyright (C) Laboratory of Imaging technologies,
#               Faculty of Electrical Engineering,
#               University of Ljubljana.
#
# This file is part of PyXOpto.
#
# PyXOpto is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyXOpto is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyXOpto. If not, see <https://www.gnu.org/licenses/>.
################################# End license ##################################
from typing import Tuple, List
from .mcobject import McObject, RawOption, RawOptions
[docs]def make_defines(options: List[RawOption], indent: str=None) -> str:
    '''
    Create defines for the given list of options. These defines should be
    included at the beginning of the OpenCL source file.
    Parameters
    ----------
    options: List[RawOption]
        List of options defined as [(option_name, option_value), ...].
    indent: str
        Indent to use when creating define directives. 
    Returns
    -------
    defs: str
        Options as defines, one per line.
    '''
    if indent is None:
        indent = ''
    d = []
    names = []
    if options:
        for name, value in options:
            d.append(McOption.make_define(name, value))
    return '\n'.join(d) 
[docs]def resolve_cl_options(*args: Tuple[RawOption] or List[RawOption]) \
        
-> List[RawOption]:
    '''
    Resolve collections of OpenCL options passed as input arguments.
    Check for duplicate options with different values and raise
    ValueError if the same option is defined with different values.
    Parameters
    ----------
    args: Tuple[RawOption]
        Each positional argument must be a list of options defined as
        [(name, value), ...].
    Returns
    -------
    options: List[RawOption]
        List of resolved options defined as [(name, value), ...]. 
    '''
    options = []
    for item in args:
        options.extend(item)
    options = list(set(options))
    names = []
    for name, value in options:
        if name in names:
            value_ = options[names.index(name)][1]
            raise ValueError(
                'The same option cannot be defned with different values!'
                'Found multiple definitions of option {} with values '
                '{} and {}!'.format(name, value_, value))
        names.append(name)
    return options 
[docs]def cl_value(value: str or bool or int or float) -> str:
    '''
    OpenCL representation of the option value that can be set by a define.
    Parameters
    ----------
    value: str or bool or int or float
        Python representation of the option value.
    Returns
    -------
    cl_value: str
        Representation of the option value that can be used in a
        standard C/OpenCL define.
    '''
    if isinstance(value, bool):
        value = {False:'FALSE', True:'TRUE'}.get(value)
    elif isinstance(value, int):
        value = '{:d}'.format(value)
    elif isinstance(value, float):
        value = '{:.16g}'.format(value)
    elif isinstance(value, str):
        pass
    elif value is None:
        value = ''
    else:
        raise TypeError('Option must be of str, bool, int or float type!')
    return value 
[docs]class McOption(McObject):
    '''
    Base Class of Monte Carlo kernel options that are set up through defines.
    '''
[docs]    @staticmethod
    def cl_value(value: str or bool or int or float) -> str:
        '''
        OpenCL representation of the option value that can be set by a define.
        Parameters
        ----------
        value: str or bool or int or float
            Python representation of the option value.
        Returns
        -------
        cl_value: str
            Representation of the option value that can be used in a
            standard C/OpenCL define.
        '''
        if isinstance(value, bool):
            value = {False:'FALSE', True:'TRUE'}.get(value)
        elif isinstance(value, int):
            value = '{:d}'.format(value)
        elif isinstance(value, float):
            value = 'FP_LITERAL({:.16g})'.format(value)
        elif isinstance(value, str):
            pass
        elif value is None:
            value = ''
        else:
            raise TypeError('Option must be of str, bool, int or float type!')
        return value 
[docs]    @classmethod
    def make_define(cls, name, value) -> str:
        '''
        Create and return a standard C/OpenCL define.
        Returns
        -------
        define: str
            A standard C/OpenCL define representing the option.
        '''
        if value is None:
            return '#define {}'.format(name)
        else:
            return '#define {} {}'.format(name, cls.cl_value(value)) 
    def __init__(self, name:str, value: str or bool or int or float):
        '''
        Initializes a kernel option.
        Parameters
        ----------
        name: str
            Option name
        value: str, bool, int, float
            Option value.
        '''
        self.cl_options = [(name, value)]
    def _get_name(self) -> str:
        return self.cl_options[0][0]
    name = property(_get_name, None, None, 'Option name.')
    def _get_value(self) -> str or bool or int or float:
        return self.cl_options[0][1]
    value = property(_get_value, None, None, 'Option value.')
    def __repr__(self):
        return "{}('{}', {})".format(self.__class__.__name__, 
                                   self.cl_options[0][0], self.cl_options[0][1])
    def __str__(self):
        return self.__repr__() 
[docs]class McBoolOption(McOption):
    '''
    Base Class of Monte Carlo kernel options that have a boolean type.
    '''
    def __init__(self, name: str, value: bool):
        '''
        Initializes a boolean kernel option.
        Parameters
        ----------
        name: str
            Property name
        value: bool
            Boolean value of the kernel option.
        '''
        super().__init__(name, bool(value))
[docs]    def get_define(self):
        return '#define {}'.format(self.name, self.value)  
[docs]class McIntOption(McOption):
    '''
    Base Class of Monte Carlo kernel options that have an integer type.
    '''
    def __init__(self, name: str, value: int):
        '''
        Initializes an integer kernel option.
        Parameters
        ----------
        name: str
            Property name
        value: int
            Integer value of the kernel option.
        '''
        super().__init__(name, int(value)) 
[docs]class McFloatOption(McOption):
    def __init__(self, name: str, value: float):
        '''
        Initializes a floating-point kernel option.
        Parameters
        ----------
        name: str
            Property name
        value: float
            Floating-point value of the kernel option.
        '''
        super().__init__(name, float(value)) 
[docs]class McTypeOption(McOption):
    '''
    Base Class of Monte Carlo kernel options that have a label type
    (no quotes in the define).
    '''
    def __init__(self, name: str, value: str):
        '''
        Initializes a label (no quotes in the define) kernel option.
        Parameters
        ----------
        name: str
            Property name
        value: str
            String value of the kernel option.
        '''
        super().__init__(name, str(value)) 
[docs]class McMethod(McIntOption):
    '''
    Selects the Monte Carlo simulation method:
      - Albedo Weight (0)
      - Albedo Rejection (1)
      - Microscopic Beer Lambert (2)
    A detailed description of the available methods can be found in:
    A. Sassaroli and F. Martelli
    Equivalence of four Monte Carlo methods for photon migration in turbid media
    J. Opt. Soc. Am. A / Vol. 29, No. 10 / October 2012
    Note
    ----
    Default method is Albedo Weight. The Albedo Rejection is fast but
    produces noisy results. The Microscopic Beer-Lambert is useful for
    fluence simulations with voxel size that is smaller than the mean
    free path. 
    '''
    albedo_weight = McIntOption('MC_METHOD', 0)
    aw = albedo_weight
    albedo_rejection = McIntOption('MC_METHOD', 1)
    ar = albedo_rejection
    microscopic_beer_lambert = McIntOption('MC_METHOD', 2)
    mbl = microscopic_beer_lambert
    default = albedo_weight
    def __init__(self, value: int=0):
        '''
        Initializes the Monte Carlo simulation method option.
        Parameters
        ----------
        value: int
            Allowed values are:
              - 0 for Albedo Weight
              - 1 for Albedo Rejection or,
              - 2 for Microscopic Beer-Lambert.
        '''
        if value not in (0, 1, 2):
            raise ValueError('Allowed values are 0, 1 or 2!')
        super().__init__('MC_METHOD', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McUseFluenceCache(McBoolOption):
    '''
    Turn on or off the use of OpenCL fluence cache.
    Default is off.
    Note
    ----
    Fluence cache will improve performance for large deposition voxels, where
    there is a high likelihood that consecutive weight depositions will go
    into the same accumulator. 
    '''
    on = McBoolOption('MC_USE_FLUENCE_CACHE', True)
    off = McBoolOption('MC_USE_FLUENCE_CACHE', False)
    default = off
    def __init__(self, value: bool=False):
        '''
        Initializes the fluence cache option.
        Parameters
        ----------
        value: bool
            Use True to enable fluence cache.
        '''
        super().__init__('MC_USE_FLUENCE_CACHE', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McUseHalfMath(McBoolOption):
    '''
    Turn on or off the use of OpenCL half-precision math.
    Default is off.
    Note
    ----
    Half-precision math gives performance benefit at a
    significantly reduced precision. Use this option with care.
    '''
    on = McBoolOption('MC_USE_HALF_MATH', True)
    off = McBoolOption('MC_USE_HALF_MATH', False)
    default = off
    def __init__(self, value: bool=False):
        '''
        Initializes the half-precision math kernel option.
        Parameters
        ----------
        value: bool
            Use True to enable the half-precision math or False to disable
            half-precision math.
            
        Note
        ----
        The half-precision math and native math
        :py:class:`~xopto.mcbase.mcoptions.McUseNativeMath` options are
        mutually excluding. Ony one of the two options can be enabled at any
        given time. If both options are enabled, the native math option takes
        precedence.
        '''
        super().__init__('MC_USE_HALF_MATH', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McUseNativeMath(McBoolOption):
    '''
    Turn on or off the use of OpenCL native math.
    Default is off.
    Note
    ----
    Native math usually gives some performance benefit, but might not
    be fully compliant with precisions defined by the IEEE standards. 
    '''
    on = McBoolOption('MC_USE_NATIVE_MATH', True)
    off = McBoolOption('MC_USE_NATIVE_MATH', False)
    default = off
    def __init__(self, value: bool=False):
        '''
        Initializes the native math kernel option.
        Parameters
        ----------
        value: bool
            Use True to enable native math or False to disable native math.
        '''
        super().__init__('MC_USE_NATIVE_MATH', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McIntLutMemory(McTypeOption):
    '''
    OpenCL memory type used to hold the floating-point lookup table data.
    Use one of :py:attr:`__constant` or :py:attr:`__global`.
    Default is :py:attr:`__global`.
    Note
    ----
    Selecting "__global" memory will likely lead to a significant performance
    degradation, in particular on older GPUs.
    The amount of available "__constant" memory on GPUs is typically limited
    to about 64k.
    '''
    #class global(McObject):
    #    cl_options = [('MC_INT_LUT_ARRAY_MEMORY', '__global')]
    __constant = McTypeOption('MC_INT_LUT_ARRAY_MEMORY', 'constant')
    __global = McTypeOption('MC_INT_LUT_ARRAY_MEMORY', 'global')
    default = __constant
    def __init__(self, value: str='global'):
        '''
        Initializes the integer type lookup table memory type kernel option.
        Parameters
        ----------
        value: str
            Use "global" to move integer type lookup table data to the
            __global OpenCL memory or use "constant" to move the lookup table
            data to the __constant OpenCL memory.
        '''
        value = {'global': '__global', '__global':'__global',
                 'constant':'__constant', '__constant':'__constant'}.get(value)
        if value is None:
            raise ValueError(
                'Lookup table memory type must be one of '
                '"constant" or "global", but got "{}"!'.format(value)
            )
        super().__init__('MC_INT_LUT_ARRAY_MEMORY', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McFloatLutMemory(McTypeOption):
    '''
    OpenCL memory type used to hold the floating-point lookup table data.
    Use one of "constant_mem" or "global_mem".
    Default is "global_mem".
    Note
    ----
    Selecting "global_mem" memory will likely lead to a significant performance
    degradation, in particular on older GPUs.
    The amount of available "constant_mem" memory on GPUs is typically limited
    to about 64k.
    '''
    #class global(McObject):
    #    cl_options = [('MC_FP_LUT_ARRAY_MEMORY', '__global')]
    constant_mem = McTypeOption('MC_FP_LUT_ARRAY_MEMORY', '__constant')
    global_mem = McTypeOption('MC_FP_LUT_ARRAY_MEMORY', '__global')
    default = constant_mem
    def __init__(self, value: str='global'):
        '''
        Initializes the floating-point lookup table memory type kernel option.
        Parameters
        ----------
        value: str
            Use "global_mem" to move integer lookup table data to the __global
            OpenCL memory or use "constant_mem" to move the lookup table data
            to the __constant OpenCL memory.
        '''
        value = {'global': '__global', '__global':'__global',
                 'constant':'__constant', '__constant':'__constant'}.get(value)
        if value is None:
            raise ValueError(
                'Lookup table memory type must be one of '
                '"constant" or "global", but got "{}"!'.format(value)
            )
        super().__init__('MC_FP_LUT_ARRAY_MEMORY', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McDebugMode(McBoolOption):
    '''
    Switch the kernel debug mode on or off. Use only for kernel development.
    Default is off.
    '''
    on = McBoolOption('MC_ENABLE_DEBUG', True)
    off = McBoolOption('MC_ENABLE_DEBUG', False)
    default = off
    def __init__(self, value: bool):
        '''
        Initializes the debug mode kernel option.
        Parameters
        ----------
        value: bool
            Use True to enable the debug mode or False to disable the debug mode.
        '''
        super().__init__('MC_ENABLE_DEBUG', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McUseEnhancedRng(McBoolOption):
    '''
    Run the kernel with the enhanced version of the random number generator.
    Default is off.
    '''
    on = McBoolOption('MC_USE_ENHANCED_RNG', True)
    off = McBoolOption('MC_USE_ENHANCED_RNG', False)
    default = off
    def __init__(self, value: bool):
        '''
        Initializes the enhanced random number generator mode kernel option.
        Note
        ----
        The enhanced version of random number generator is required when
        performing simulations in large scattering volumes, were the number of
        scattering events is expected to be on the order of millions.
        Depending on the OpenCL device, there might be a small performance
        cost of about 15%, which is the reason for disabling this option
        by default.
        Parameters
        ----------
        value: bool
            Use True to enable the enhanced random number generator mode or
            False to disable the enhanced random number generator mode.
        '''
        super().__init__('MC_USE_ENHANCED_RNG', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McUseSoft64Atomics(McBoolOption):
    '''
    Force software implementation of 64-bit atomic operations.
    '''
    on = McBoolOption('MC_USE_SOFT_64_ATOMICS', True)
    off = McBoolOption('MC_USE_SOFT_64_ATOMICS', False)
    default = off
    def __init__(self, value: bool):
        '''
        Initializes the software-based 64-bit integer atomics usage option.
        Parameters
        ----------
        value: bool
            Use True to force software-implemented 64-bit integer atomics.
        '''
        super().__init__('MC_USE_SOFT_64_ATOMICS', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McUseLottery(McBoolOption):
    '''
    Switch on or off photon packet termination by lottery.
    Default is on.
    '''
    on = McBoolOption('MC_USE_LOTTERY', True)
    off = McBoolOption('MC_USE_LOTTERY', False)
    default = on
    def __init__(self, value: bool):
        '''
        Initializes the lottery kernel option.
        Parameters
        ----------
        value: bool
            Use True to enable the lottery or False to disable the lottery.
        '''
        super().__init__('MC_USE_LOTTERY', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McMinimumPacketWeight(McFloatOption):
    '''
    Minimum packet weight allowed before starting packet termination or lottery.
    Default value is 1e-4.
    '''
    default = McFloatOption('MC_PACKET_WEIGHT_MIN', 1e-4)
    def __init__(self, value: float=1e-4):
        if 0.0 >= value or value > 1.0:
            raise ValueError(
                'Minimum photon packet weight must be > 0.0 and <= 1.0, '
                'but got {}!'.format(value)
            )
        super().__init__('MC_PACKET_WEIGHT_MIN', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McPacketLotteryChance(McFloatOption):
    '''
    Terminate photon packet by lottery if the value of a uniform random
    number from [0, 1] exceeds this value.
    Default value is 0.1.
    '''
    default = McFloatOption('MC_PACKET_LOTTERY_CHANCE', 0.1)
    def __init__(self, value: float=0.1):
        '''
        Initializes the lottery chance kernel option.
        Parameters
        ----------
        value: float
            Lottery survival fraction from 0.0 to 1.0.
        '''
        if  0.0 > value or value > 1.0:
            raise ValueError(
                'Lottery chance must be a value from 0.0 to 1.0, '
                'but got {}!'.format(value)
            )
        super().__init__('MC_PACKET_LOTTERY_CHANCE', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McUsePackedStructures(McBoolOption):
    '''
    Turn on/off the use of tightly packed structures.
    Default value is off.
    Note
    ----
    Note that packed structures can lead to significant performance
    degradation of the Monte Carlo kernel. This option is the last resort if
    the fields of the OpenCL and host structures cannot be properly aligned.
    When declaring OPenCL or host structures always start with the
    largest data type and move towards smaller data types. Use data types
    that are of size no less than 4 bytes.
    '''
    on = McBoolOption('MC_USE_PACKED_STRUCTURES', True)
    off = McBoolOption('MC_USE_PACKED_STRUCTURES', False)
    default = off
    def __init__(self, value: bool):
        '''
        Initializes the packed structures kernel option.
        Parameters
        ----------
        value: bool
            Use True to enable packed structures or
            False to disable packed structures.
        '''
        super().__init__('MC_USE_PACKED_STRUCTURES', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
[docs]class McUseEvents(McBoolOption):
    '''
    Turn on/off tracking of packet events. Default value is off.
    '''
    on = McBoolOption('MC_USE_EVENTS', True)
    off = McBoolOption('MC_USE_EVENTS', False)
    default = off
    def __init__(self, value: bool):
        '''
        Initializes the kernel option for tracking packet events.
        Parameters
        ----------
        value: bool
            Use True to enable tracking of packet events or
            False to disable tracking of packet events.
        '''
        super().__init__('MC_USE_PACKED_STRUCTURES', value)
    def __repr__(self):
        return '{}({})'.format(self.__class__.__name__, self.cl_options[0][1]) 
Options = List[McOption]