Source code for sdpb_python.numbers

"""Conversions between Python numbers and the decimal strings SDPB exchanges.

Strings are printed with ``max_digits10`` digits for the working precision, so
``mpf -> str -> BigFloat -> str -> mpf`` is lossless.
"""

from __future__ import annotations

import math
from fractions import Fraction
from numbers import Integral, Rational, Real
from typing import Any, Iterable

import mpmath

NumberLike = Any  # int, float, str, Fraction, mpmath.mpf, ...


[docs] def digits_for(bits: int) -> int: """Decimal digits that uniquely identify a binary float of ``bits`` bits.""" return math.ceil(bits * math.log10(2)) + 1
[docs] def to_str(value: NumberLike, bits: int) -> str: """Render ``value`` as a decimal string exact to ``bits`` of precision.""" if isinstance(value, str): s = value.strip() if not s: raise ValueError("empty number string") if "/" in s: # "1/12": exact fraction, rendered at the working precision return to_str(Fraction(s), bits) return s if isinstance(value, bool): return "1" if value else "0" if isinstance(value, Integral): return str(int(value)) if isinstance(value, Fraction) or (isinstance(value, Rational) and not isinstance(value, Real)): with mpmath.workprec(bits): return mpmath.nstr(mpmath.mpf(value.numerator) / value.denominator, digits_for(bits)) if isinstance(value, float): # Shortest decimal that round-trips the double: 0.1 means "0.1", # not 0.1000000000000000055511151231257827. return repr(value) if isinstance(value, mpmath.mpf): with mpmath.workprec(max(bits, value.context.prec)): return mpmath.nstr(value, digits_for(max(bits, value.context.prec))) if hasattr(value, "_mpf_"): return to_str(mpmath.mpf(value._mpf_), bits) try: # sympy numbers, numpy scalars, decimal.Decimal, ... return to_str(mpmath.mpf(str(value)), bits) except Exception as exc: raise TypeError(f"cannot convert {type(value).__name__} to a number") from exc
[docs] def from_str(s: str, bits: int) -> mpmath.mpf: """Parse a decimal string without rounding below ``bits`` of precision.""" with mpmath.workprec(max(bits, mpmath.mp.prec)): return mpmath.mpf(s)
[docs] def strs(values: Iterable[NumberLike], bits: int) -> list[str]: return [to_str(v, bits) for v in values]
[docs] def mpfs(values: Iterable[str], bits: int) -> list[mpmath.mpf]: return [from_str(v, bits) for v in values]
[docs] def matrix_strs(rows: Any, bits: int) -> list[list[str]]: """Convert a nested sequence / numpy array / mpmath.matrix to string rows.""" if isinstance(rows, mpmath.matrix): return [[to_str(rows[i, j], bits) for j in range(rows.cols)] for i in range(rows.rows)] if hasattr(rows, "tolist"): rows = rows.tolist() return [[to_str(v, bits) for v in row] for row in rows]
[docs] def matrix_mpfs(data: dict, bits: int) -> mpmath.matrix: """From the Cython layer's {"shape": (h, w), "rows": [[str]]}.""" h, w = data["shape"] m = mpmath.matrix(h, w) for i, row in enumerate(data["rows"]): for j, v in enumerate(row): m[i, j] = from_str(v, bits) return m