Beispiel #1
0
    def create_key(self,
                   base_ring,
                   arg1=None,
                   arg2=None,
                   sparse=None,
                   order='degrevlex',
                   names=None,
                   name=None,
                   implementation=None,
                   degrees=None):
        """
        Create the key under which a free algebra is stored.

        TESTS::

            sage: FreeAlgebra.create_key(GF(5),['x','y','z'])
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'],3)
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),3,'xyz')
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'], implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'],3, implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),3,'xyz', implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),3,'xyz', implementation='letterplace', degrees=[1,2,3])
            ((1, 2, 3), Multivariate Polynomial Ring in x, y, z, x_ over Finite Field of size 5)

        """
        if arg1 is None and arg2 is None and names is None:
            # this is used for pickling
            if degrees is None:
                return (base_ring, )
            return tuple(degrees), base_ring
        # test if we can use libSingular/letterplace
        if implementation == "letterplace":
            args = [arg for arg in (arg1, arg2) if arg is not None]
            kwds = dict(sparse=sparse, order=order, implementation="singular")
            if name is not None:
                kwds["name"] = name
            if names is not None:
                kwds["names"] = names
            PolRing = PolynomialRing(base_ring, *args, **kwds)
            if degrees is None:
                return (PolRing, )
            from sage.all import TermOrder
            T = PolRing.term_order() + TermOrder('lex', 1)
            varnames = list(PolRing.variable_names())
            newname = 'x'
            while newname in varnames:
                newname += '_'
            varnames.append(newname)
            R = PolynomialRing(PolRing.base(),
                               varnames,
                               sparse=sparse,
                               order=T)
            return tuple(degrees), R
        # normalise the generator names
        from sage.all import Integer
        if isinstance(arg1, (Integer, int)):
            arg1, arg2 = arg2, arg1
        if not names is None:
            arg1 = names
        elif not name is None:
            arg1 = name
        if arg2 is None:
            arg2 = len(arg1)
        names = normalize_names(arg2, arg1)
        return base_ring, names
Beispiel #2
0
    def create_key(self,base_ring, arg1=None, arg2=None,
                                      sparse=False, order='degrevlex',
                                      names=None, name=None,
                                      implementation=None, degrees=None):
        """
        Create the key under which a free algebra is stored.

        TESTS::

            sage: FreeAlgebra.create_key(GF(5),['x','y','z'])
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'],3)
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),3,'xyz')
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'], implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'],3, implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),3,'xyz', implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),3,'xyz', implementation='letterplace', degrees=[1,2,3])
            ((1, 2, 3), Multivariate Polynomial Ring in x, y, z, x_ over Finite Field of size 5)

        """
        if arg1 is None and arg2 is None and names is None:
            # this is used for pickling
            if degrees is None:
                return (base_ring,)
            return tuple(degrees),base_ring
        PolRing = None
        # test if we can use libSingular/letterplace
        if implementation is not None and implementation != 'generic':
            try:
                PolRing = PolynomialRing(base_ring, arg1, arg2,
                                   sparse=sparse, order=order,
                                   names=names, name=name,
                                   implementation=implementation if implementation != 'letterplace' else None)
                if not isinstance(PolRing, MPolynomialRing_libsingular):
                    if PolRing.ngens() == 1:
                        PolRing = PolynomialRing(base_ring, 1, PolRing.variable_names())
                        if not isinstance(PolRing, MPolynomialRing_libsingular):
                            raise TypeError
                    else:
                        raise TypeError
            except (TypeError, NotImplementedError) as msg:
                raise NotImplementedError("The letterplace implementation is not available for the free algebra you requested")
        if PolRing is not None:
            if degrees is None:
                return (PolRing,)
            from sage.all import TermOrder
            T = PolRing.term_order() + TermOrder('lex',1)
            varnames = list(PolRing.variable_names())
            newname = 'x'
            while newname in varnames:
                newname += '_'
            varnames.append(newname)
            return tuple(degrees),PolynomialRing(PolRing.base(), varnames,
                    sparse=sparse, order=T,
                    implementation=implementation if implementation != 'letterplace' else None)
        # normalise the generator names
        from sage.all import Integer
        if isinstance(arg1, (int, long, Integer)):
            arg1, arg2 = arg2, arg1
        if not names is None:
            arg1 = names
        elif not name is None:
            arg1 = name
        if arg2 is None:
            arg2 = len(arg1)
        names = sage.structure.parent_gens.normalize_names(arg2, arg1)
        return base_ring, names
Beispiel #3
0
    def create_key(self, base_ring, arg1=None, arg2=None,
            sparse=None, order='degrevlex',
            names=None, name=None,
            implementation=None, degrees=None):
        """
        Create the key under which a free algebra is stored.

        TESTS::

            sage: FreeAlgebra.create_key(GF(5),['x','y','z'])
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'],3)
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),3,'xyz')
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'], implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'],3, implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),3,'xyz', implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),3,'xyz', implementation='letterplace', degrees=[1,2,3])
            ((1, 2, 3), Multivariate Polynomial Ring in x, y, z, x_ over Finite Field of size 5)

        """
        if arg1 is None and arg2 is None and names is None:
            # this is used for pickling
            if degrees is None:
                return (base_ring,)
            return tuple(degrees),base_ring
        # test if we can use libSingular/letterplace
        if implementation == "letterplace":
            args = [arg for arg in (arg1, arg2) if arg is not None]
            kwds = dict(sparse=sparse, order=order, implementation="singular")
            if name is not None:
                kwds["name"] = name
            if names is not None:
                kwds["names"] = names
            PolRing = PolynomialRing(base_ring, *args, **kwds)
            if degrees is None:
                return (PolRing,)
            from sage.all import TermOrder
            T = PolRing.term_order() + TermOrder('lex',1)
            varnames = list(PolRing.variable_names())
            newname = 'x'
            while newname in varnames:
                newname += '_'
            varnames.append(newname)
            R = PolynomialRing(
                    PolRing.base(), varnames,
                    sparse=sparse, order=T)
            return tuple(degrees), R
        # normalise the generator names
        from sage.all import Integer
        if isinstance(arg1, (Integer,) + integer_types):
            arg1, arg2 = arg2, arg1
        if not names is None:
            arg1 = names
        elif not name is None:
            arg1 = name
        if arg2 is None:
            arg2 = len(arg1)
        names = normalize_names(arg2, arg1)
        return base_ring, names
    def create_key(self,base_ring, arg1=None, arg2=None,
                                      sparse=False, order='degrevlex',
                                      names=None, name=None,
                                      implementation=None, degrees=None):
        """
        Create the key under which a free algebra is stored.

        TESTS::

            sage: FreeAlgebra.create_key(GF(5),['x','y','z'])
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'],3)
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),3,'xyz')
            (Finite Field of size 5, ('x', 'y', 'z'))
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'], implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),['x','y','z'],3, implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),3,'xyz', implementation='letterplace')
            (Multivariate Polynomial Ring in x, y, z over Finite Field of size 5,)
            sage: FreeAlgebra.create_key(GF(5),3,'xyz', implementation='letterplace', degrees=[1,2,3])
            ((1, 2, 3), Multivariate Polynomial Ring in x, y, z, x_ over Finite Field of size 5)

        """
        if arg1 is None and arg2 is None and names is None:
            # this is used for pickling
            if degrees is None:
                return (base_ring,)
            return tuple(degrees),base_ring
        PolRing = None
        # test if we can use libSingular/letterplace
        if implementation is not None and implementation != 'generic':
            try:
                PolRing = PolynomialRing(base_ring, arg1, arg2,
                                   sparse=sparse, order=order,
                                   names=names, name=name,
                                   implementation=implementation if implementation != 'letterplace' else None)
                if not isinstance(PolRing, MPolynomialRing_libsingular):
                    if PolRing.ngens() == 1:
                        PolRing = PolynomialRing(base_ring, 1, PolRing.variable_names())
                        if not isinstance(PolRing, MPolynomialRing_libsingular):
                            raise TypeError
                    else:
                        raise TypeError
            except (TypeError, NotImplementedError) as msg:
                raise NotImplementedError("The letterplace implementation is not available for the free algebra you requested")
        if PolRing is not None:
            if degrees is None:
                return (PolRing,)
            from sage.all import TermOrder
            T = PolRing.term_order() + TermOrder('lex',1)
            varnames = list(PolRing.variable_names())
            newname = 'x'
            while newname in varnames:
                newname += '_'
            varnames.append(newname)
            return tuple(degrees),PolynomialRing(PolRing.base(), varnames,
                    sparse=sparse, order=T,
                    implementation=implementation if implementation != 'letterplace' else None)
        # normalise the generator names
        from sage.all import Integer
        if isinstance(arg1, (int, long, Integer)):
            arg1, arg2 = arg2, arg1
        if not names is None:
            arg1 = names
        elif not name is None:
            arg1 = name
        if arg2 is None:
            arg2 = len(arg1)
        names = sage.structure.parent_gens.normalize_names(arg2, arg1)
        return base_ring, names
Beispiel #5
0
class Operator(object):
    ### Static parameters
    _op_preference = 0

    #######################################################
    ### INIT METHOD AND GETTERS
    #######################################################
    def __init__(self, base, input, derivate = foo_derivative):
        '''
        This method allows the user to instantiate a new object of type Operator. 
        
        This method must be extended in each child-class of Operator.
        
        INPUT:
            - base: the Structure where the coefficients of the operator will be
            - input: the input data for the operator. The format must be checked in each class.
        '''
        
        ## Computing the polynomial associated ring
        try:
            self.__pol_var = 'n'
            i = -1
            names = [str(gen) for gen in base.base_ring().gens()]
            while(self.__pol_var in names):
                i += 1
                self.__pol_var = 'n' + str(i)
            
            self.__polynomialRing = PolynomialRing(base.base_ring(), self.__pol_var)
        except RuntimeError:
            self.__pol_var = 'n'
            self.__polynomialRing = PolynomialRing(base.base_ring(),self.__pol_var)
            
        ## Computing the Ring_w_Sequence associated to base
        if(isinstance(base, Ring_w_Sequence)):
            self.__base = base
        else:
            self.__base = Wrap_w_Sequence_Ring(base)
        self._original_base = base
                
        ## Declaring private fields
        self.__derivate = derivate
        
    def base(self):
        return self.__base
        
    def derivate(self):
        return self.__derivate
        
    def _get_preference(self):
        return self._op_preference
    
    ### Getter methods
    def order(self):
        '''
        This method allows the user to get the order of the operator. 
        
        This method must be extended in each child-class of Operator.
        '''
        raise NotImplementedError('Method not implemented -- Abstract class asked')
        
    def coefficients(self):
        '''
        This method allows the user to get the coefficients of the operator. 
        
        This method must be extended in each child-class of Operator.
        '''
        raise NotImplementedError('Method not implemented -- Abstract class asked')
    
    def coefficient(self, i):
        if (i < 0):
            raise IndexError('The argument must be a number greater or equal to zero')
        elif (i <= self.order()):
            return self.coefficients()[i]
        else:
            return 0
        
    @cached_method
    def companion(self):
        field = self.base().fraction_field()
        
        coefficients = [field(el) for el in self.coefficients()]
        
        ## We divide by the leading coefficient
        coefficients = [-(coefficients[i]/coefficients[-1]) for i in range(len(coefficients)-1)]
        ## Trying to reduce the elements
        try:
            for i in range(len(coefficients)):
                coefficients[i].reduce()
        except (AttributeError, ArithmeticError):
            pass
        d = len(coefficients)
        
        ## Building the rows of our matrix
        rows = [[0 for i in range(d-1)] + [coefficients[0]]]
        for i in range(d-1):
            rows += [[kronecker_delta(i,j) for j in range(d-1)] + [coefficients[i+1]]]
            
        ## Returning the matrix
        return Matrix(field, rows)
    
    @cached_method
    def noetherian_ring(self, other=None):
        r'''
            Method that builds a noetherian ring that contains all the coefficients

            This method computes a differential noetherian ring in Sage that contains all
            the coefficients of ``self`` and ``other`` and where we can divide
            be the leading coefficients of both operators. This method will
            rely on the method :func:`~ajpastor.dd_functions.ddFunction.DDFunction.noetherian_ring`.

            This method currently does not work if ``self.base()`` is a DDRing.

            INPUT:
                * ``other``: other operator to compute the common noetherian ring at the same time
                * ``structure``: determines the format of the output.

            OUTPUT:

            The output have always 3 main entries:
                * The ring `R` that is the base of the Noetherian extension
                * The list of elements `\alpha_i` that will generate the final ring.
                * The list of generators of `S`.
            In the case of ``structure`` being ``True``, a fourth entry will be added
            with the ring `R[x_1,\ldots,x_n]_S` where the variable `x_i` represent the 
            element `\alpha_i`.
        '''
        from ajpastor.dd_functions import is_DDRing
        if(is_DDRing(self.base())):
            raise NotImplementedError('Method not implemented. Class: %s' %self.__class__)

        c = [self.coefficients()[-1]]
        if(not (other is None)): c += [other.coefficients()[-1]]
        final_dens = []
        for el in c:
            if((not el in self.base()) or (not self.base()(el).is_unit())):
                final_dens += [el]
        
        return self.base(), [], final_dens

    #######################################################
    
    #######################################################
    ### RECURSION POLYNOMIALS METHODS
    #######################################################        
    def get_recursion_polynomial(self, n):
        '''
            Method to get a recursion polynomial associated with this operator.
            
            If the requested polynomial is greater than zero, then we will return a `forward` polynomial, but if the request is lesser than zero, we will return a backward polynomial, computing it if necessary.
            
            INPUT:
                - n: index of the recursion polynomial requested.
                
            OUTPUT:
                A polynomial on self.base()[nx] where x will be an integer such the variable nx is not in self.base().gens()
        '''
        if(n >= 0):
            try:
                return self.forward(n)
            except IndexError:
                return 0
        else:
            return self.backward(-n)
            
    @cached_method
    def forward(self, n):
        if(n < 0):
            raise IndexError("Forward polynomials have only positive index")
        elif(n > self.order()):
            return 0
        elif(self.is_zero()):
            return 0
        else: 
            var = self.__polynomialRing.gens()[-1]
            
            return sum([self.base().sequence(self.coefficient(l),l-n)*falling_factorial(var+n,l) for l in range(n, self.order()+1)])
          
    @cached_method  
    def backward(self, n):
        if(n < 0):
            raise IndexError("Backward polynomials have only positive index")
        elif(self.is_zero()):
            return 0
        
        var = self.__polynomialRing.gens()[-1]
        return sum([self.base().sequence(self.coefficient(l), l+n)*falling_factorial(var-n, l) for l in range(0,self.order()+1)])
        
    def get_recursion_row(self,i):
        r = self.coefficients()
        d = self.order()
        row = []
        
        #var = self.__polynomialRing.gens()[-1]
        ### First summation part
        #row += [sum([falling_factorial(k,l)*self.base().sequence(r[l],i-k+l) for l in range(0,k+1)]) for k in range(0,min(i,d))]
        #if(i<d):
        #    ## Second summation part
        #    row += [sum([falling_factorial(k+i,l)*self.base().sequence(r[l], l-k) for l in range(k,i+k+1)]) for k in range(d-i)]
        #    ## Third summation part
        #    row += [self.forward(k)(**{str(var):i}) for k in range(d-i,d+1)]
        #else:
        #    ## Second summation part
        #    row += [self.backward(i-d-k)(**{str(var):i}) for k in range(i-d)]
        #    ## Third summation part
        #    row += [self.forward(k)(**{str(var):i}) for k in range(0,d+1)]
        
        ## First summation part
        row += [sum([falling_factorial(k,l)*self.base().sequence(r[l],i-k+l) for l in range(0,k+1)]) for k in range(0,min(i,d))]
        if(i<d):
            ## Second summation part
            row += [sum([falling_factorial(k+i,l)*self.base().sequence(r[l], l-k) for l in range(k,i+k+1)]) for k in range(d-i)]
            ## Third summation part
            row += [self.__eval_pol(self.forward(k),i) for k in range(d-i,d+1)]
        else:
            ## Second summation part
            row += [self.__eval_pol(self.backward(i-d-k),i) for k in range(i-d)]
            ## Third summation part
            row += [self.__eval_pol(self.forward(k),i) for k in range(0,d+1)]
            
        return row
        
    def get_recursion_matrix(self, n):
        nrows = n+1
        ncols = n+self.forward_order+1
        rows = []
        
        for i in range(nrows):
            row = self.get_recursion_row(i)
            
            rows += [[row[i] for i in range(min(len(row),ncols))] + [0 for i in range(ncols-len(row))]]
            
        return Matrix(self.__polynomialRing.base(), rows)
    #######################################################
        
    #######################################################
    ### SOLUTION SPACE METHODS
    #######################################################
    @derived_property
    def dimension(self):
        return self.jp_matrix().right_kernel_matrix().nrows()
    
    @derived_property
    def forward_order(self):
        if(self.is_zero()):
            raise ValueError("The zero operator has not forward order")
        
        n = self.order()
        while(self.get_recursion_polynomial(n) == 0):
            n -= 1
        
        return n
    
    @cached_method
    def jp_value(self):
        ## TODO Be careful with this computation: only valid is the base field are the rational
        jp_pol = self.get_recursion_polynomial(self.forward_order)
        return max([self.order()-self.forward_order] + get_integer_roots(jp_pol))
       
    @cached_method
    def jp_matrix(self):
        return self.get_recursion_matrix(self.jp_value())
        
    def get_jp_fo(self):
        return self.jp_value()+self.forward_order
    #######################################################
    
    #######################################################
    ### OPERATOR ARITHMETIC METHODS (ABSTRACT)
    ####################################################### 
    def add(self, other):
        '''
        This method allows the user to add two operators. 
        
        This method must be extended in each child-class of Operator.
        '''
        raise NotImplementedError('Method not implemented -- Abstract class asked')
        
    def scalar(self, other):
        '''
        This method allows the user to do a (left)scalar multiplication. 
        
        This method must be extended in each child-class of Operator.
        '''
        raise NotImplementedError('Method not implemented -- Abstract class asked')
        
    def mult(self, other):
        '''
        This method allows the user to multiply two operators. 
        
        This method must be extended in each child-class of Operator.
        '''
        raise NotImplementedError('Method not implemented -- Abstract class asked')
        
    def is_zero(self):
        '''
        This method allows the user to know if this operator is the zero operator. 
        
        This method must be extended in each child-class of Operator.
        '''
        raise NotImplementedError('Method not implemented -- Abstract class asked')
        
    def derivative(self):
        '''
        This method allows the user to derivate the operator (if possible). If not, it will raise a
        NotImplementedError. 
        
        This method must be extended in each child-class of Operator.
        '''
        raise NotImplementedError('This operator can not be derivated')
    ####################################################### 
    
    ####################################################### 
    ### SOLUTION ARITHMETHIC METHODS
    ####################################################### 
    def add_solution(self, other):
        '''
        This method computes a new operator such any solution of 'self == 0' plus any solution of 'other == 0' must satisfy.
        '''
        ## If the input is not an operator, trying the casting
        if(not isinstance(other, Operator)):
            other = self.__class__(self.base(), other, self.derivate())
            
        ## If the input is an Operator we guess the hightest preference type
        if(not isinstance(other, self.__class__)):
            if(other._get_preference() > self._get_preference()):
                return other.add_solution(self)
            other = self.__class__(self.base(), other, self.derivate())
            
        return self._compute_add_solution(other)
                
    def mult_solution(self, other):
        '''
        This method computes a new operator such any solution of 'self == 0' multiplied by any solution of 'other == 0' must satisfy.
        '''
        ## If the input is not an operator, trying the casting
        if(not isinstance(other, Operator)):
            other = self.__class__(self.base(), other, self.derivate())
            
        ## If the input is an Operator we guess the hightest preference type
        if(not isinstance(other, self.__class__)):
            if(other._get_preference() > self._get_preference()):
                return other.mult_solution(self)
            other = self.__class__(self.base(), other, self.derivate())
            
        return self._compute_mult_solution(other)
        
    def derivative_solution(self):
        '''
        This method computes a new operator such the derivative of any solution of 'self == 0' must satisfy.
        '''
        return self._compute_derivative_solution()
        
    def integral_solution(self):
        '''
        This method computes a new operator such any anti-derivative of any solution of 'self == 0' must satisfy.
        '''
        return self._compute_integral_solution()
        
    def compose_solution(self, other):
        '''
        Let c be the coefficients of 'self'. This method computes a differential operator such any solution 'f' of the equation with coefficients 'c(other^(-1))' composed with 'other' will satisfy.
        
        This method (awkward by definition) requires that 'other' is in self.base().
        
        INPUT:
            - other: an element of 'self.base()'
            
        OUTPUT:
            - A new operator 'A' of the same class as 'self' with 'A.base() == self.base()'
        
        ERRORS:
            - 'TypeError' wil be raised if 'other' is not in 'self.base()'
        '''
        ## If the input is not an operator, trying the casting
        if(not other in self.base()):
            raise TypeError("Element (%s) is not valid for compose with a solution of %s" %(other, str(self)))
        
        return self._compute_compose_solution(other)
    
    def _compute_add_solution(self, other):
        raise NotImplementedError('Method not implemented. Class: %s' %self.__class__)
        
    def _compute_mult_solution(self, other):
        '''
        This method computes a new operator such any solution of 'self == 0' multiplied by any solution of 'other == 0' must satisfy.
        It assumes that other and self are exactly the same type.
        '''
        raise NotImplementedError('Method not implemented. Class: %s' %self.__class__)
        
    def _compute_derivative_solution(self):
        '''
        This method computes a new operator such the derivative of any solution of 'self == 0' must satisfy.
        '''
        raise NotImplementedError('Method not implemented. Class: %s' %self.__class__)
        
    def _compute_integral_solution(self):
        '''
        This method computes a new operator such any anti-derivative of any solution of 'self == 0' must satisfy.
        '''
        raise NotImplementedError('Method not implemented. Class: %s' %self.__class__)
        
    def _compute_compose_solution(self, other):
        '''
        This method computes a new operator that annihilates any solution of 'self' compose with any solution of 'other'.
        '''
        raise NotImplementedError('Method not implemented. Class: %s' %self.__class__)
    #######################################################

    ####################################################### 
    ### SIMPLE ARITHMETHIC METHODS
    ####################################################### 
    def simple_add_solution(self, other):
        r'''
            Method to compute the annihilator of the addition of solutions to two operators.

            This method computes a new operator `A` that annihilates all the functions that 
            are the addition of the solutions of ``self`` and ``other``.
            This method takes care that the singularities of the new equation are the same 
            as the singularities of the input. 
            
            Currently this only work if the coefficients are univariate polynomials.
        '''
        ## If the input is not an operator, trying the casting
        if(not isinstance(other, Operator)):
            other = self.__class__(self.base(), other, self.derivate())
            
        ## If the input is an Operator we guess the hightest preference type
        if(not isinstance(other, self.__class__)):
            if(other._get_preference() > self._get_preference()):
                return other.add_solution(self)
            other = self.__class__(self.base(), other, self.derivate())
            
        return self._compute_simple_add_solution(other)

    def simple_mult_solution(self, other):
        r'''
            Method to compute the annihilator of the product of solutions to two operators.

            This method computes a new operator `A` that annihilates all the functions that 
            are the product of the solutions of ``self`` and ``other``.
            This method takes care that the singularities of the new equation are the same 
            as the singularities of the input. 
            
            Currently this only work if the coefficients are univariate polynomials.
        '''
        ## If the input is not an operator, trying the casting
        if(not isinstance(other, Operator)):
            other = self.__class__(self.base(), other, self.derivate())
            
        ## If the input is an Operator we guess the hightest preference type
        if(not isinstance(other, self.__class__)):
            if(other._get_preference() > self._get_preference()):
                return other.mult_solution(self)
            other = self.__class__(self.base(), other, self.derivate())
            
        return self._compute_simple_mult_solution(other)

    def simple_derivative_solution(self):
        r'''
            Method to compute the annihilator of the derivative of solutions to ``self``.

            This method computes a new operator `A` that annihilates all the functions that 
            are the derivative of the solutions of ``self``.
            This method takes care that the singularities of the new equation are the same 
            as the singularities of the input. 
            
            Currently this only work if the coefficients are univariate polynomials.
        '''
        return self._compute_simple_derivative_solution()

    def _compute_simple_add_solution(self, other, bound=5):
        raise NotImplementedError('Method not implemented. Class: %s' %self.__class__)

    def _compute_simple_mult_solution(self, other, bound=5):
        raise NotImplementedError('Method not implemented. Class: %s' %self.__class__)

    def _compute_simple_derivative_solution(self, bound=5):
        raise NotImplementedError('Method not implemented. Class: %s' %self.__class__)

    ####################################################### 
    
    ####################################################### 
    ### MAGIC PYTHON METHODS
    ####################################################### 
    def __getitem__(self, key):
        if(key > self.order() or key < 0):
            raise IndexError("No coefficient can be get bigger than the order or with negative index")
        return self.coefficient(key)
    
    def __call__(self, obj):
        '''
        This method allows the user to apply the operator to an object. 
        
        This method must be extended in each child-class of Operator.
        '''
        try:
            res = obj.parent().zero()
            to_mult = obj
            for i in range(self.order()+1):
                res += self.coefficient(i)*to_mult
                to_mult = to_mult.derivative()
            return res
        except Exception:
            raise NotImplementedError('Method not implemented -- Abstract class asked')
        
    def __hash__(self):
        return hash(tuple(self.coefficients()))
    ### Addition
    def __add__(self, other):
        try:
            return self.add(other)
        except Exception:
            return NotImplemented
            
    ### Substraction
    def __sub__(self, other):
        try:
            #return self.add(other.scalar(-1))
            return self.scalar(-1).add(other).scalar(-1)
        except Exception:
            return NotImplemented
            
    ### Multiplication
    def __mul__(self, other):
        try:                
            return self.mult(other)
        except Exception:
            return NotImplemented

    def __pow__(self, other):
        if(not other in ZZ or other <= 0):
            return NotImplemented
        if(other == 1):
            return self
        else:
            return self.__mul__(self.__pow__(other-1))
  
    ### Reverse addition
    def __radd__(self, other):
        return self.__add__(other)
            
    ### Reverse substraction
    def __rsub__(self, other):
        return self.scalar(-1).__add__(other)
            
    ### Reverse multiplication
    def __rmul__(self, other):
        try:
            if(not isinstance(other, Operator)):
                return self.scalar(other)
                
            return other.mult(self)
        except Exception:
            return NotImplemented
        
    ### Implicit addition
    def __iadd__(self, other):
        return self.__add__(other)
            
    ### Implicit substraction
    def __isub__(self, other):
        return self.__sub__(other)
            
    ### Implicit multiplication
    def __imul__(self, other):
        return self.__mul__(other)
            
    ### Unary '-'
    def __neg__(self):
        return self.scalar(-1)
    ####################################################### 
    
    def __eval_pol(self,pol, val):
        try:
            return pol(**{self.__pol_var:val})
        except RuntimeError:
            coeffs = pol.coefficients(False)
            return sum(coeffs[i]*val**i for i in range(len(coeffs)))