r""" 
VICE Callback Functions 
=======================

.. warning:: User access of this module is discouraged. 

Implements callable objects to allow VICE's C library to call functions passed 
to it from python. These objects raise warnings in the case that a function 
returns a forbidden or non-numerical value and returns a default value in 
these cases. 

Contents 
--------
numerical : ``decorator`` 
	Forces a function to return 0 if it returns a non-numerical value. 
no_nan : ``decorator`` 
	Forces a function to return 0 if it returns ``NaN``. 
no_inf : ``decorator`` 
	Forces a function to return 0 if it returns ``inf``. 
positive : ``decorator`` 
	Forces a function to return 1e-12 if it returns a negative or zero value. 
callback1 : ``object`` 
	The base class for functions of one numerical value. 
	__call__ function has ``numerical`` decorator. 
callback1_nan : ``callback1`` 
	A derived class for functions accepting one numerical value. 
	__call__ function has ``numerical`` and ``no_nan`` decorators. 
callback1_nan_inf : ``callback1`` 
	A derived class for functions accepting one numerical value. 
	__call__ function has ``numerical``, ``no_nan``, and ``no_inf`` decorators. 
callback1_nan_positive: ``callback1`` 
	A derived class for functions accepting one numerical value. 
	__call__ function has ``numerical``, ``no_nan``, and ``positive`` 
	decorators. 
callback1_nan_inf_positive : ``callback1`` 
	A derived class for functions accepting one numerical value. 
	__call__ function has ``numerical``, ``no_nan``, ``no_inf``, and 
	``positive`` decorators. 
callback2 : ``object`` 
	The base class for functions of two numerical values. 
	__call__ function has ``numerical`` decorator. 
callback2_nan : ``callback2`` 
	A derived class for functions accepting two numerical values. 
	__call__ function has ``numerical`` and ``no_nan`` decorators. 
callback2_nan_inf : ``callback2`` 
	A derived class for functions accepting two numerical values. 
	__call__ function has ``numerical``, ``no_nan``, and ``no_inf`` decorators. 
callback2_nan_positive : ``callback2`` 
	A derived class for functions accepting two numerical values. 
	__call__ function has ``numerical``, ``no_nan``, and ``positive`` 
	decorators. 
callback2_nan_inf_positive : ``callback2`` 
	A derived class for functions accepting two numerical values. 
	__call__ function has ``numerical``, ``no_nan``, ``no_inf``, and 
	``positive`` decorators. 

Notes 
-----
The callback1 and callback2 objects are implemented separately from one 
another; this is motivated by the fact that the C library expects a given 
function to accept a specific number of parameters. To mirror this level of 
strictness, the number of arguments each callback object allows is defined 
explicitly rather than in one callback object accepting ``*args`` as a 
parameter. 

This has the positive side effect of user's being notified via a TypeError 
when a function with the wrong number of arguments is passed. Absent this, 
users would instead see suppression messages generated by Cython. This makes 
VICE more user-friendly. 
""" 

from __future__ import absolute_import 
from .._globals import ScienceWarning 
from . import _pyutils 
import functools 
import math as m 
import warnings 
import numbers 


def numerical(function): 
	r""" 
	Type : ``decorator`` 

	.. warning:: User access of this decorator is discouraged. 

	Raises a ``ScienceWarning`` and returns 0 when a function returns a 
	non-numerical value. 
	""" 
	@functools.wraps(function) 
	def wrapper(*args): 
		y = function(*args) 
		if isinstance(y, numbers.Number): 
			return float(y) 
		else: 
			warnings.warn("""\
Function %s evaluated to non-numerical value at %s. Suppressing TypeError by \
returning 0. Checking simulation output for numerical consistency is \
advised.""" % (str(function), str(args)), ScienceWarning) 
			return 0 
	return wrapper 


def no_nan(function): 
	r""" 
	Type : ``decorator`` 

	.. warning:: User access of this decorator is discouraged. 

	Raises a ``ScienceWarning`` and returns 0 when a function returns ``NaN``. 
	""" 
	@functools.wraps(function) 
	def wrapper(*args): 
		y = function(*args) 
		if m.isnan(y): 
			warnings.warn("""\
Function %s evaluated to NaN at %s. Suppressing ArithmeticError by returning \
0. Checking simulation output for numerical consistency is advised.""" % (
				str(function), str(args)), ScienceWarning) 
			return 0 
		else: 
			return y 
	return wrapper 


def no_inf(function): 
	r""" 
	Type : ``decorator`` 

	.. warning:: User access of this decorator is discouraged. 

	Raises a ``ScienceWarning`` and returns 0 when a function returns ``inf``. 
	""" 
	@functools.wraps(function) 
	def wrapper(*args): 
		y = function(*args) 
		if m.isinf(y): 
			warnings.warn("""\
Function %s evaluated to inf at %s. Suppressing ArithmeticError by returning \
0. Checking simulation output for numerical consistency is advised.""" % (
				str(function), str(args)), ScienceWarning) 
			return 0 
		else: 
			return y 
	return wrapper 


def positive(function): 
	r""" 
	Type : ``decorator`` 

	.. warning:: User access of this decorator is discouraged. 

	Raises a ``ScienceWarning`` and returns 1e-12 when a function returns 
	a negative or zero value. 
	""" 
	@functools.wraps(function) 
	def wrapper(*args): 
		y = function(*args) 
		if y <= 0: 
			warnings.warn("""\
Function %s evaluated to non-positive value at %s. Suppressing ArithmeticError \
by returning 1e-12. Checking simulation output for numerical consistency is \
advised.""" % (str(function), str(args)), ScienceWarning) 
			return 1e-12 
		else: 
			return y 
	return wrapper 


class callback1: 

	r""" 
	The base class of the callback object for one numerical parameter. 
	__call__ function has the ``numerical`` decorator. 

	.. warning:: User access of this class is discouraged. 

	Parameters 
	----------
	function : <function> 
		The attribute ``function``. 

	Attributes 
	----------
	function : <function> 
		The mathematical function of one numerical value. 
	""" 
	def __init__(self, function): 
		self.function = function 

	@numerical 
	def __call__(self, x): 
		return self._function(x) 

	def __repr__(self): 
		""" 
		Ensures that the name of the function the user defined prints rather 
		than the name of this class, for clearer error messages. 
		""" 
		return self._function.__name__ 

	def __str__(self): 
		""" 
		Ensures that the name of the function the user defined prints rather 
		than the name of this class, for clearer error messages. 
		""" 
		return self._function.__name__ 

	@property 
	def function(self): 
		""" 
		Type : <function> 

		The function to call, passed from the user. Will default to 0 or 
		1e-12 in the event the function returns a forbidden value, depending 
		on the decorators on the __call__ function. 
		""" 
		return self._function 

	@function.setter 
	def function(self, value): 
		if callable(value): 
			if _pyutils.arg_count(value) == 1: 
				self._function = value 
			else: 
				raise TypeError("""Attribute 'function' must accept exactly \
one positional argument.""") 
		else: 
			raise TypeError("""Attribute 'function' must be a callable \
object. Got: %s""" % (type(value))) 


class callback1_nan(callback1): 

	r""" 
	A derived class of the callback object for one numerical parameter. 
	__call__ function has the ``numerical`` and ``no_nan`` decorators. 

	.. warning:: User access of this class is discouraged. 

	.. seealso:: ``callback1`` 
	""" 

	@no_nan 
	@numerical 
	def __call__(self, x): 
		return self._function(x) 


class callback1_nan_inf(callback1): 

	r""" 
	A derived class of the callback object for one numerical parameter. 
	__call__ function has the ``numerical``, ``no_nan``, and ``no_inf`` 
	decorators. 

	.. warning:: User access of this class is discouraged. 

	.. seealso:: ``callback1`` 
	""" 

	@no_inf 
	@no_nan 
	@numerical 
	def __call__(self, x): 
		return self._function(x) 


class callback1_nan_positive(callback1): 

	r""" 
	A derived class of the callback object for one numerical parameter. 
	__call__ function has the ``numerical``, ``no_nan``, and ``positive`` 
	decorators. 

	.. warning:: User access of this class is discouraged. 

	.. seealso:: ``callback1`` 
	""" 

	@positive 
	@no_nan 
	@numerical 
	def __call__(self, x): 
		return self._function(x) 


class callback1_nan_inf_positive(callback1): 

	r""" 
	A derived class of the callback object for one numerical parameter. 
	__call__ function has the ``numerical``, ``no_nan``, ``no_inf``, and 
	``positive`` decorators. 

	.. warning:: User access of this class is discouraged. 

	.. seealso:: ``callback1`` 
	""" 

	@positive 
	@no_inf 
	@no_nan 
	@numerical 
	def __call__(self, x): 
		return self._function(x) 


class callback2: 

	r""" 
	The base class of the callback object for two numerical parameters. 
	__call__ function has the ``numerical`` decorator. 

	.. warning:: User access of this class is discouraged. 

	Parameters 
	----------
	function : <function> 
		The attribute ``function``. 

	Attributes 
	----------
	function : <function> 
		The mathematical function of two numerical values. 
	"""
	
	def __init__(self, function): 
		self.function = function 

	@numerical 
	def __call__(self, x, y): 
		return self._function(x, y) 

	def __repr__(self): 
		""" 
		Ensures that the name of the function the user defined prints rather 
		than the name of this class, for clearer error messages. 
		""" 
		return self._function.__name__ 

	def __str__(self): 
		""" 
		Ensures that the name of the function the user defined prints rather 
		than the name of this class, for clearer error messages. 
		""" 
		return self._function.__name__ 

	@property 
	def function(self): 
		""" 
		Type : <function> 

		The function to call, passed from the user. Will default to 0 or 
		1e-12 in the event the function returns a forbidden value, depending 
		on the decorators on the __call__ function. 
		""" 
		return self._function 

	@function.setter 
	def function(self, value): 
		if callable(value): 
			if _pyutils.arg_count(value) == 2: 
				self._function = value 
			else: 
				raise TypeError("""Attribute 'function' must accept exactly \
two positional arguments.""") 
		else: 
			raise TypeError("""Attribute 'function' must be a callable \
object. Got: %s""" % (type(value))) 


class callback2_nan(callback2): 

	r""" 
	A derived class of the callback object for two numerical parameters. 
	__call__ function has the ``numerical`` and ``no_nan`` decorators. 

	.. warning:: User access of this class is discouraged. 

	.. seealso:: ``callback1`` 
	""" 

	@no_nan 
	@numerical 
	def __call__(self, x, y): 
		return self._function(x, y) 


class callback2_nan_inf(callback2): 

	r""" 
	A derived class of the callback object for two numerical parameters. 
	__call__ function has the ``numerical``, ``no_nan``, and ``no_inf`` 
	decorators. 

	.. warning:: User access of this class is discouraged. 

	.. seealso:: ``callback1`` 
	""" 

	@no_inf 
	@no_nan 
	@numerical 
	def __call__(self, x, y): 
		return self._function(x, y) 


class callback2_nan_positive(callback2): 

	r""" 
	A derived class of the callback object for two numerical parameters. 
	__call__ function has the ``numerical``, ``no_nan``, and ``positive`` 
	decorators. 

	.. warning:: User access of this class is discouraged. 

	.. seealso:: ``callback1`` 
	""" 

	@positive 
	@no_nan 
	@numerical 
	def __call__(self, x, y): 
		return self._function(x, y) 


class callback2_nan_inf_positive(callback2): 

	r""" 
	A derived class of the callback object for two numerical parameters. 
	__call__ function has the ``numerical``, ``no_nan``, ``no_inf``, and 
	``positive`` decorators. 

	.. warning:: User access of this class is discouraged. 

	.. seealso:: ``callback1`` 
	""" 

	@positive 
	@no_inf 
	@no_nan 
	@numerical 
	def __call__(self, x, y): 
		return self._function(x, y) 

