# This file is part of Bertini 2.
#
# python/bertini/_coefficients.py 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.
#
# python/bertini/_coefficients.py 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 python/bertini/_coefficients.py. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright(C) Bertini2 Development Team
#
# See <http://www.gnu.org/licenses/> for a copy of the license,
# as well as COPYING. Bertini2 is provided with permitted
# additional terms in the b2/licenses/ directory.
"""Exact-coefficient coercion for building systems.
The single home for turning exact values into function-tree coefficient *nodes*
(:func:`coefficient` / :func:`coefficients`) and into multiprecision coefficient *matrices* (the
private ``_exact_to_mpfr`` / ``_coerce_mpfr_matrix`` used by the ``System`` block methods).
**Coefficients must be exact.** Python ints (including numpy integers) and bertini nodes work
directly inside numpy arithmetic, so an *integer* matrix needs nothing special. For any other
coefficient use :func:`coefficient` / :func:`coefficients`, which accept ``fractions.Fraction``,
exact strings (``'2.5'`` decimal or ``'3/4'`` rational), and bertini multiprecision values. Python
``float`` and ``complex`` are **refused**: a 64-bit float literal would inject only ~16 correct
digits into the arbitrary-precision function tree, silently capping downstream precision.
"""
import numpy as np
from fractions import Fraction
from bertini.symbolics import Integer, Rational, Complex
from bertini import multiprec as _mp
from bertini._pybertini.function_tree import AbstractNode as _AbstractNode
# bertini multiprecision value types (not function-tree nodes) that we accept as exact
# coefficients by wrapping them in a Complex node.
_MP_VALUE_TYPES = tuple(
t for t in (getattr(_mp, n, None) for n in ('real_mp', 'complex_mp', 'int_mp', 'rational_mp'))
if isinstance(t, type)
)
[docs]
def coefficient(value):
"""Coerce a single exact value into a function-tree coefficient node.
Accepts bertini nodes (returned unchanged), ``int`` / numpy integers,
``fractions.Fraction``, exact strings (``'2.5'`` or ``'3/4'``), and bertini
multiprecision values. Raises ``TypeError`` for Python ``float`` / ``complex`` (and
numpy floating types) -- a floating-point literal would cap precision.
There are four ways to spell an *exact* non-integer coefficient::
>>> from fractions import Fraction
>>> import bertini
>>> from bertini import multiprec
>>> _ = bertini.coefficient('2.5') # exact decimal string
>>> _ = bertini.coefficient('3/4') # exact rational string
>>> _ = bertini.coefficient(Fraction(3, 4)) # a fractions.Fraction
>>> _ = bertini.coefficient(multiprec.complex_mp('0.1')) # a full-precision multiprec value
A Python ``float`` is refused: ``bertini.coefficient(0.1)`` raises ``TypeError``. An exact
*complex* coefficient is a ``multiprec.complex_mp`` with real and imaginary parts:
``bertini.coefficient(multiprec.complex_mp('0.6', '0.8'))``.
"""
if isinstance(value, _AbstractNode):
return value
if isinstance(value, bool):
# bool is a subclass of int; refuse it explicitly rather than coercing True -> 1.
raise TypeError("a bool is not a valid coefficient")
if isinstance(value, (int, np.integer)):
return Integer(int(value))
if isinstance(value, Fraction):
return Rational(str(value)) # 'p/q', or 'p' when the denominator is 1
if isinstance(value, str):
return Rational(value) if '/' in value else Complex(value)
if _MP_VALUE_TYPES and isinstance(value, _MP_VALUE_TYPES):
return Complex(value)
if isinstance(value, (float, complex, np.floating, np.complexfloating)):
raise TypeError(
f"refusing to use the Python {type(value).__name__} {value!r} as a coefficient: "
"a floating-point literal carries only ~16 digits and would cap the precision of "
"the arbitrary-precision function tree. Pass an exact value instead -- an int, a "
"fractions.Fraction, an exact string such as '2.5' or '3/4', or a "
"bertini.multiprec value."
)
raise TypeError(f"cannot use {type(value).__name__} as a coefficient")
[docs]
def coefficients(array_like):
"""Coerce an array (or nested list) of exact values to an object array of coefficient nodes.
Use this to bring a non-integer matrix or vector into the function tree::
A = bertini.coefficients([['5/2', '1'], ['0', '3']]) # exact rationals
A = bertini.coefficients(fraction_matrix) # fractions.Fraction entries
Integer arrays do not need this: numpy ``int * Variable`` already builds ``Integer``
coefficients. Refuses Python floats (see :func:`coefficient`).
"""
arr = np.array(array_like, dtype=object)
out = np.empty(arr.shape, dtype=object)
flat_in = arr.reshape(-1)
flat_out = out.reshape(-1)
for i in range(flat_in.size):
flat_out[i] = coefficient(flat_in[i])
return out
def _exact_to_mpfr(value):
"""Convert a single exact value to a multiprec.complex_mp (refusing Python floats).
Mirrors :func:`coefficient`'s exact-only rule, but produces a multiprecision *value*
(for a coefficient matrix) rather than a function-tree node. Accepts the same exact
spellings as :func:`coefficient`, including a bertini constant *node* -- e.g. a
``symbolics.Complex`` -- so a value good enough for :func:`coefficient` is good enough
here too (issue #326).
"""
# A function-tree constant node (as coefficient() / Complex() / Integer() / Rational()
# produce): accept it, the symmetric partner to the complex_mp case below. A Complex node
# carries a full-precision complex_mp directly; any other constant node is evaluated (it
# holds no variables). This closes the gap where a complex_mp was accepted but the Complex
# *node* wrapping the same value was not (issue #326).
if isinstance(value, _AbstractNode):
if isinstance(value, Complex):
return _mp.complex_mp(value.value())
try:
return _mp.complex_mp(value.eval())
except Exception as e:
raise TypeError(
"cannot use a non-constant node as a coefficient (it still contains "
f"variables): {value!r}"
) from e
if _MP_VALUE_TYPES and isinstance(value, _MP_VALUE_TYPES):
return _mp.complex_mp(value)
if isinstance(value, bool):
raise TypeError("a bool is not a valid coefficient")
if isinstance(value, (int, np.integer)):
return _mp.complex_mp(str(int(value)))
if isinstance(value, Fraction):
return _mp.complex_mp(str(value.numerator)) / _mp.complex_mp(str(value.denominator))
if isinstance(value, str):
if '/' in value:
num, den = value.split('/')
return _mp.complex_mp(num) / _mp.complex_mp(den)
return _mp.complex_mp(value)
if isinstance(value, (float, complex, np.floating, np.complexfloating)):
raise TypeError(
f"refusing to use the Python {type(value).__name__} {value!r} as a coefficient: "
"a floating-point literal would cap the precision of the block. Pass an exact "
"value -- an int, a fractions.Fraction, an exact string, or a bertini.multiprec "
"value."
)
raise TypeError(f"cannot use {type(value).__name__} as a coefficient")
def _coerce_mpfr_matrix(coefficients, context):
"""Coerce a (possibly nested list / numpy) array of exact values to an mpfr_complex matrix.
Returns ``(numpy_object_matrix, num_rows, num_cols)``. Refuses Python floats.
"""
rows = [list(r) for r in coefficients]
if not rows:
raise ValueError(f"{context} must have at least one row")
ncol = len(rows[0])
M = np.empty((len(rows), ncol), dtype=_mp.complex_mp)
for i, row in enumerate(rows):
if len(row) != ncol:
raise ValueError(f"{context} is ragged (rows of differing length)")
for j, entry in enumerate(row):
M[i, j] = _exact_to_mpfr(entry)
return M, len(rows), ncol
def _coerce_mpfr_vector(values, context):
"""Coerce a 1-D sequence of exact values to an mpfr_complex vector.
Returns ``(numpy_object_vector, length)``. Refuses Python floats (same exact-value
contract as :func:`_coerce_mpfr_matrix`).
"""
entries = list(values)
if not entries:
raise ValueError(f"{context} must have at least one entry")
v = np.empty(len(entries), dtype=_mp.complex_mp)
for i, entry in enumerate(entries):
v[i] = _exact_to_mpfr(entry)
return v, len(entries)