๐Ÿงฎ Canonical ordering, and opting outยถ

Bertini hash-conses the function tree: identical subexpressions become a single shared node. For that sharing to fire on expressions you build separately, the library also canonically orders the operands of every sum and product โ€“ so x + y and y + x are not just equal, they are the same node. This tutorial shows what that ordering does, how to choose it, and how to turn it off when you want to keep the exact operand order you wrote.

On by defaultยถ

Commutative reorderings collapse, coefficients sort to the front of a monomial, and the constant term of a sum stays last โ€“ so printed expressions read the conventional way.

x, y = Variable('x'), Variable('y')

assert str(x + y) == str(y + x) == 'x+y'      # commutative: one canonical form (and one node)
assert str(x * y) == str(y * x) == 'x*y'
assert str(3 * x**2) == '3*x^2'               # the coefficient leads its monomial
assert str(x**2 + 2*x - 1) == 'x^2+2*x-1'     # degree-descending; the constant term is last

You can ask whether canonicalization is on, and toggle it, with bertini.canonicalize().

assert bertini.canonicalize() is True         # on by default

Choosing the monomial orderยถ

The order is a pluggable monomial order โ€“ Lex, RevLex, or GrevLex (the default, graded so higher total degree leads). It is a session-global setting, because it determines which expressions share a node.

bertini.monomial_order(MonomialOrder.Lex)
lex = str(x**2 + y**3)                         # lexicographic: x before y
bertini.monomial_order(MonomialOrder.GrevLex)
grev = str(x**2 + y**3)                        # graded: the higher-degree y^3 leads

assert lex == 'x^2+y^3'
assert grev == 'y^3+x^2'

bertini.monomial_order(MonomialOrder.GrevLex)  # restore the default

Opting outยถ

Canonicalization is the right default โ€“ it maximizes sharing and keeps the compiled straight-line program small. But sometimes you have arranged the operands of an expression deliberately, and you want the library to leave that arrangement alone. Turn canonicalization off while you build that expression, and the authored operand order is preserved:

bertini.canonicalize(False)                    # opt out
try:
    authored = y + x
    assert str(authored) == 'y+x'              # exactly as written, not reordered to x+y
finally:
    bertini.canonicalize(True)                 # back to the default for everything else

Why would you do this? Floating-point addition and multiplication are not associative: the order in which operands are combined changes the rounding. If you have written an expression so that a catastrophically-cancelling pair is combined first (or so that a product avoids an intermediate overflow), canonical reordering could undo that arrangement. Opting out hands you back exact control of the operand order for the numerically-sensitive part of a model, while everything you do not opt out of stays canonicalized โ€“ and keeps sharing.

Note

The toggle is session-global, so the idiom is โ€œopt out, build the sensitive expression, opt back inโ€ (as above). Everything built while it is off keeps its authored order and is a distinct node from the canonical form; everything built while it is on is canonicalized and shared.

Complete exampleยถ

The whole tutorial as one runnable script โ€“ assemble nothing, just run it:

canonical_ordering.pyยถ
"""Canonical ordering, and opting out -- Bertini 2 tutorial.

Assembled from the ``.. testcode::`` blocks of the "Canonical ordering, and
opting out" tutorial into one runnable program.

Run:  python canonical_ordering.py
"""

import bertini
from bertini import Variable, MonomialOrder


def on_by_default():
    """Commutative reorderings collapse and printed expressions read the conventional way."""
    x, y = Variable('x'), Variable('y')

    assert str(x + y) == str(y + x) == 'x+y'      # commutative: one canonical form (and one node)
    assert str(x * y) == str(y * x) == 'x*y'
    assert str(3 * x**2) == '3*x^2'               # the coefficient leads its monomial
    assert str(x**2 + 2*x - 1) == 'x^2+2*x-1'     # degree-descending; the constant term is last

    assert bertini.canonicalize() is True         # on by default

    return x, y


def choosing_the_monomial_order(x, y):
    """The pluggable, session-global monomial order determines which expressions share a node."""
    bertini.monomial_order(MonomialOrder.Lex)
    lex = str(x**2 + y**3)                         # lexicographic: x before y
    bertini.monomial_order(MonomialOrder.GrevLex)
    grev = str(x**2 + y**3)                        # graded: the higher-degree y^3 leads

    assert lex == 'x^2+y^3'
    assert grev == 'y^3+x^2'

    bertini.monomial_order(MonomialOrder.GrevLex)  # restore the default


def opting_out(x, y):
    """Turn canonicalization off to preserve a deliberately authored operand order."""
    bertini.canonicalize(False)                    # opt out
    try:
        authored = y + x
        assert str(authored) == 'y+x'              # exactly as written, not reordered to x+y
    finally:
        bertini.canonicalize(True)                 # back to the default for everything else


def main():
    x, y = on_by_default()
    choosing_the_monomial_order(x, y)
    opting_out(x, y)


if __name__ == '__main__':
    main()