Skip to content

ENH: Formatting Polynomials #8893

@philer

Description

@philer

TL;DR Add a way of turning numpy.polynomial.polynomial.Polynomial instances into a human readable and/or machine parseable string, such as

  • 2x³ + x² - 1 (utf8)
  • 2x^3 + x^2 - 1 (ascii)
  • 2x^{3} + x^{2} - 1 (latex)
  • 2x<sup>3</sup> + x<sup>2</sup> - 1 (html)
  • 2*x**3 + x**2 - 1 (python)

Disclaimer: I'm relatively new to numpy so if this is just completely out of the question please bear with me and let me know. If this issue finds approval I'd be happy to implement it myself. :)

Motivation

Numpy is used for scientific work all around. In literature polynomials are typically written in the form 2x³ + x² - 1 which I will refer to as "human readable". It is characterized by descending exponents and omission of unnecessary parts involving coefficients and exponents ±1 and 0. Yet neither str nor format return a version close to the above, instead displaying the "machine oriented" internal representation as an array of coefficients sorted by ascending index. This representation can be confusing for people who only know the human readable form and in some cases completely impractical (e.g. for sparse polynomials of high degree, like x¹²³). It would be very useful for authors who employ numpy to be able to generate output directly in a human readable format or in a common markup language (such as LaTeX and HTML). This allows automation and prevents common slipups when manually translating between the formats.

Possible approaches

I can think of several ways to incorporate a formatting option into numpy. Personally I prefer option 1) as it is the most pythonic and I can not see any significant downsides assuming the default is chosen to avoid backwards compatibility issues at least for upcoming minor versions. Some of these options may also coexist.

1) Implement Polynomial.__format__ with a reasonable default. In this case "reasonable" could mean either defaulting to str if no style option is provided (in the interest of backward compatibility), or defaulting to one of the versions in the TL;DR example.

As a result the following code:

p = Polynomial([-1, 0, 1, 2])
print(format(p))       # default to str(p)
print("{:ascii}".format(p))
print(f"{p :utf8}")    # Python 3.6+

… could output:

poly([-1.  0.  1.  2.])
2x^3 + x^2 - 1
2x³ + x² - 1

2) Add an explicit method like Polynomial.human_readable that takes some optional parameters (see below).

3) Add a standalone function such as numpy.polynomials.polyutils.format_polynomial which isn't very OOP but at least it exists and it's out of the way.

4) Extend numpy.set_printoptions to allow specifying a formatter for Polynomials. This means of course that the actual implementation of the human readable formatting isn't part of numpy but that it is at least easy to add without monkey patching the Polynomial class.

Demo implementation

I've written a brief sample implementation that can be used to demonstrate the suggested behavior of 1). The monkey-patched Polynomial.__format__ only supports the exponent style option described above, while format_polynomial() demonstrates some additional options. You can run the code as is in Python 3. The UTF-8 literals break Python 2 but substituting a compatible implementation should not be an issue.

exp_styles = {
    'utf8':   lambda n: "".join("⁰¹²³⁴⁵⁶⁷⁸⁹"[int(k)] for k in str(n)),
    'ascii':  "^{:d}".format,
    'latex':  "^{{{:d}}}".format,
    'html':   "<sup>{:d}</sup>".format,
    'python': "**{:d}".format,
}

def format_polynomial(polynomial, variable="x", mul="", exponents=exp_styles['utf8']):
    """
    Format a polynomial given as iterable in a human readable string.

    Parameters:
        polynomial : array_like
        variable : string, optional
            To be used instead of "x" in the formatted output.
        mul : string, optional
            To be used as a multiplication symbol in the output
        exponents : function (str -> str), optional
            Render the exponent from a given integer > 0.

    Examples:
    Format in a Python parseable format:
        >>> s = format_polynomial([-1, 0, 1, 2], "5", "*", "**{}".format)
        >>> print(s)
        2*5**3 + 5**2 - 1
        >>> eval(s)
        274
    """
    if len(polynomial) == 1:
        return format(polynomial[0], "g")
    string = sign = ""
    exps = ("", variable,
            *(variable + exponents(n) for n in range(2, len(polynomial))))
    for n, (coef, exp) in enumerate(zip(polynomial, exps)):  # right to left
        if coef:
            string = exp + sign + string
            if abs(coef) != 1 or n == 0:
                string = format(abs(coef), "g") + (mul if n else "") + string
            sign = " - " if coef < 0 else " + "
    return "-" + string if sign == " - " else string


def _Polynomial__format__(self, style):
    """Monkey patch for Polynomial.__format__"""
    if not style:
        return str(self)
    return format_polynomial(self, exponents=exp_styles[style])

from numpy.polynomial.polynomial import Polynomial
Polynomial.__format__ = _Polynomial__format__


p = Polynomial([-1, 0, 1, 2])
print(format(p))       # default to str(p)
print("{:ascii}".format(p))
# print(f"{p :utf8}")    # Python 3.6+

That's it. Please let me know what you think. 👍

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions