#
##
##  SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.4  (Thu Nov 16 18:07:39 CET 2023)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program 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.
##
##  This program 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 this program.  If not, see http://www.gnu.org/licenses/.
##
#
"""Polynomials
This module defines the class Polynomial, representing a polynomial
in n variables.
"""
import numpy as np
from pyformex import arraytools as at
[docs]class Polynomial():
    """A polynomial in `n` dimensions.
    An n-dimensional polynomial is the sum of `nterms` terms, where each
    term is the product of a coefficient (a float constant) and a monomial
    in the `n` variables. A monomial is the product of each of the variables
    raised to a specific exponent. For example, in a 2-dim space, a
    polynomial in (x,y) could be::
        2 + 3 * x - x * y - y**2
    This contains four terms. We store the monomials as an int array with
    shape (nterms, ndim) specifying for each term the exponents of each of
    the variables in the monomial. For the above example this becomes:
    [(0,0), (1,0), (1,1), 0,1)]. The `nterms` coefficients are stored
    as floats: [2.0, 3.0, -1.0, -1.0].
    Parameters
    ----------
    exp: :term:`array_like`
        An int array of shape (nterms,ndim) with the exponents of each of
        the ndim variables in each of the nterms terms of the polynomial.
    coeff: :term:`array_like`
        A float array with the coefficients of the terms.
        If not specified, all coefficients are set to 1.
    symbol:str, optional
        A string of length at least `ndim` with the symbols to be used
        for each of the `ndim` independent variables. The default
        is set for a 3-d polynomial.
    Examples
    --------
    >>> p = Polynomial([(0,0),(1,0),(1,1),(0,2)],(2,3,-1,-1))
    >>> p.exp
    array([[0, 0],
           [1, 0],
           [1, 1],
           [0, 2]])
    >>> p.coeff
    array([ 2.,  3., -1., -1.])
    >>> print(p.atoms())
    ['1', 'x', 'x*y', 'y**2']
    >>> print(p)
    2.0 +3.0*x -1.0*x*y -1.0*y**2
    >>> print(repr(p))
    Polynomial([[0, 0], [1, 0], [1, 1], [0, 2]], [2.0, 3.0, -1.0, -1.0], 'xyz')
    >>> print(Polynomial([(2,0), (1,1), (0,2)], (1,2,1), 'ab'))
    a**2 +2.0*a*b +b**2
    """
    def __init__(self, exp, coeff=None, symbol='xyz'):
        """Create an n-d polynomial"""
        self.exp = at.checkArray(exp, kind='i', ndim=2)
        if coeff is None:
            self.coeff = np.ones(self.nterms)
        else:
            self.coeff = at.checkArray(coeff, (self.nterms,), 'f', 'i')
        if len(symbol) < self.ndim:
            raise ValueError(
                f"Insufficient symbols for {self.ndim}-dim Polynimial")
        self.symbol = symbol
    @property
    def nterms(self):
        return self.exp.shape[0]
    @property
    def ndim(self):
        return self.exp.shape[1]
[docs]    def degrees(self):
        """Return the degree of the polynomial in each of the dimensions.
        The degree is the maximal exponent for each of the dimensions.
        """
        return self.exp.max(axis=0) 
[docs]    def degree(self):
        """Return the total degree of the polynomial.
        The degree is the sum of the degrees for all dimensions.
        """
        return self.degrees().sum() 
[docs]    def atoms(self):
        """Return a human representation of the monomials
        Returns
        -------
        list of str
            A list of the monomials in the Polynomial
        Examples
        --------
        >>> Polynomial([(0,0),(1,0),(1,1),(0,2)]).atoms()
        ['1', 'x', 'x*y', 'y**2']
        """
        return [monomial(e, self.symbol) for e in self.exp] 
[docs]    def human(self):
        """Return a human representation
        Returns
        -------
        str:
            A string representation of the Polynomial
        Examples
        --------
        >>> Polynomial([(0,0),(1,0),(1,1),(0,2)]).human()
        '1 +x +x*y +y**2'
        """
        mon = self.atoms()
        mon = [str(c)+'*'+m if c != 1 else m for c, m in zip(self.coeff, mon)]
        return ' +'.join(mon).replace('*1', '').replace('+-', '-') 
    __str__ = human
    def __repr__(self):
        return (f"Polynomial({self.exp.tolist()}, {self.coeff.tolist()}, "
                f"{self.symbol!r})")
[docs]    def evalAtoms(self, x):
        """Evaluate the monomials at the given points
        Parameters
        ----------
        x: :term:`array_like`
            An (npoints,ndim) array of points where the polynomial is to
            be evaluated.
        Returns
        -------
        array
            The (npoints,nterms) array of values of the `nterms` monomials
            at the `npoints` points.
        Examples
        --------
        >>> p = Polynomial([(0,0),(1,0),(1,1),(0,2)],(2,3,-1,-1))
        >>> p.evalAtoms([[1,2],[3,0],[2,1]])
        array([[1., 1., 2., 4.],
               [1., 3., 0., 0.],
               [1., 2., 2., 1.]])
        """
        x = at.checkArray(x, (-1, self.ndim), 'f', 'i')
        g = dict([(self.symbol[i], x[:, i]) for i in range(self.ndim)])
        atoms = self.atoms()
        aa = np.zeros((len(x), len(atoms)), at.Float)
        for k, a in enumerate(atoms):
            aa[:, k] = eval(a, g)
        return aa 
[docs]    def eval(self, x):
        """Evaluate the polynomial at the given points
        Parameters
        ----------
        x: :term:`array_like`
            An (npoints,ndim) array of points where the polynomial is to
            be evaluated.
        Returns
        -------
        array
            The value of the polynomial at the `npoints` points.
        Examples
        --------
        >>> p = Polynomial([(0,0),(1,0),(1,1),(0,2)],(2,3,-1,-1))
        >>> print(p.eval([[1,2],[3,0],[2,1]]))
        [-1. 11.  5.]
        """
        terms = self.evalAtoms(x)
        return (self.coeff*terms).sum(axis=-1)  
# def polynomial(atoms, x, y=0, z=0):
#     """Build a matrix of functions of coords.
#     - `atoms`: a list of text strings representing a mathematical function of
#       `x`, and possibly of `y` and `z`.
#     - `x`, `y`, `z`: a list of x- (and optionally y-, z-) values at which the
#       `atoms` will be evaluated. The lists should have the same length.
#     Returns a matrix with `nvalues` rows and `natoms` colums.
#     """
#     aa = np.zeros((len(x), len(atoms)), Float)
#     for k, a in enumerate(atoms):
#         aa[:, k] = eval(a)
#     return aa
[docs]def factor(sym, exp):
    """Return a string with a variable to some exponent
    Parameters
    ----------
    sum: str
        The name of the variable
    exp: int
        The exponent to which the variable is raised
    Returns
    -------
    str:
        The expression with the variable `sym` raised to the power `exp`.
    Examples
    --------
    >>> factor('x', 3), factor('y', 1), factor('z', 0)
    ('x**3', 'y', '1')
    """
    if exp == 0:
        return '1'
    elif exp == 1:
        return sym
    else:
        return sym+'**'+str(exp) 
[docs]def monomial(exp, symbol='xyz'):
    """Return the monomial for the given exponents.
    Parameters
    ----------
    exp: tuple of int
        The integer exponents to which to raise the ndim independent variables.
    symbol: str
        A string of at least the same length as `exp`, containing the
        variables to use for each of the ndim independent variables.
        The default is set for a 3-dimensional space.
    Returns
    -------
    str
        A string representation of a monomial created by raising
        the symbols to the corresponding exponent.
    Examples
    --------
    >>> monomial((2,1))
    'x**2*y'
    >>> monomial((0,3,2))
    'y**3*z**2'
    """
    factors = [factor(symbol[i], j) for i, j in enumerate(exp)]
    # Join and sanitize (note we do not have '**1')
    return '*'.join(factors).replace('1*', '').replace('*1', '') 
# End