"""The `precision` module is used when wanting to
adjust `~kanon.units.radices.BasedReal` arithmetical operations behavior.
All operations are made within a `PrecisionContext` rules, which indicate :
- A `TruncatureMode`
- A `PrecisionMode`
- 4 `CustomArithmeticAlgorithm`, (add, sub, mul, div)
Default precision context is set to `TruncatureMode.NONE`, `PrecisionMode.MAX`, and all
`CustomArithmeticAlgorithm` as default.
To set new precision rules you should use the `set_precision` context manager. In
the example below, I set the precision so that the result significant number is 0
and that it should be truncated.
>>> from kanon.units import Sexagesimal
>>> a = Sexagesimal("1;50")
>>> b = Sexagesimal("2;0")
>>> a + b
03 ; 50
>>> with set_precision(tmode=TruncatureMode.TRUNC, pmode=0):
... a + b
...
03 ;
If you want to use a specific algorithm for one of the arithmetical operations,
you first need to define the algorithm with this signature with the `identify_func`
decorator specifying a unique ID :
.. code:: python
Callable[[PreciseNumber, PreciseNumber], PreciseNumber]
For example, a multiplication algorithm which is essentialy
equivalent to ``a * round(b,0)`` with `TEST_MUL` as its ID:
>>> @identify_func("TEST_MUL")
... def test_mul(a: PreciseNumber, b: PreciseNumber) -> PreciseNumber:
... res = 0
... for i in range(int(round(b,0))):
... res += a
... return res
You can now use this function to make multiplications use this algorithm
>>> b * a
03 ; 40
>>> with set_precision(mul=test_mul):
... b * a
04 ; 00
All operations and their associated context are stored inside the `ContextPrecision`
when the recording flag is set to ``True``. You can either set it to ``True`` inside
of a `set_precision` context manager, or globally turn it on with `set_recording(True)`.
Records are displayed with `get_records`, and can be cleared with `clear_records`.
Let's try to record our operations.
>>> set_recording(True)
>>> get_records()
[]
>>> a + b
03 ; 50
>>> with set_precision(tmode=TruncatureMode.ROUND, pmode=1):
... a + Sexagesimal("2;5,30")
03 ; 56
>>> with set_precision(mul=test_mul):
... b * a
04 ; 00
>>> get_records()
[{'args': (01 ; 50, 02 ; 00, '+', 03 ; 50), 'tmode': 'NONE', 'pmode': 'MAX', 'add': \
'DEFAULT', 'sub': 'DEFAULT', 'mul': 'DEFAULT', 'div': 'DEFAULT'}, {'args': (01 ; 50, \
02 ; 05,30, '+', 03 ; 56), 'tmode': 'ROUND', 'pmode': 1, 'add': 'DEFAULT', 'sub': \
'DEFAULT', 'mul': 'DEFAULT', 'div': 'DEFAULT'}, {'args': (02 ; 00, 01 ; 50, '*', 04 ; \
00), 'tmode': 'NONE', 'pmode': 'MAX', 'add': 'DEFAULT', 'sub': 'DEFAULT', 'mul': \
'TEST_MUL', 'div': 'DEFAULT'}]
>>> clear_records()
>>> set_recording(False)
>>> a + b
03 ; 50
>>> get_records()
[]
Types
-----
.. py:attribute:: CustomArithmeticAlgorithm
:type: Optional[Callable[[PreciseNumber, PreciseNumber], PreciseNumber]
"""
from __future__ import annotations
import abc
from contextlib import contextmanager
from dataclasses import asdict, dataclass, field
from enum import Enum
from functools import partial, wraps
from numbers import Number
from typing import Callable, Dict, List, Literal, Optional, TypeVar, Union
__all__ = [
"PrecisionMode",
"TruncatureMode",
"set_precision",
"PrecisionContext",
"PreciseNumber",
"get_context",
"set_context",
"set_recording",
"get_records",
"clear_records",
"CustomArithmeticAlgorithm",
"Truncable",
]
def _with_context_precision(func=None, symbol=None):
if not func:
return partial(_with_context_precision, symbol=symbol)
@wraps(func)
def wrapper(*args, **kwargs) -> "Truncable":
with set_precision(recording=False):
value: "Truncable" = func(*args, **kwargs)
if len(args) != 2 or any(not isinstance(a, Truncable) for a in args):
return value
if not isinstance(value, Truncable):
raise TypeError
value = value.resize(args[0]._get_significant(args[1]))
ctx = get_context()
value = ctx.tmode(value)
ctx.record(*args, symbol, value)
return value
return wrapper
TTruncable = TypeVar("TTruncable", bound="Truncable")
TPreciseNumber = TypeVar("TPreciseNumber", bound="PreciseNumber")
[docs]class Truncable(metaclass=abc.ABCMeta):
[docs] @abc.abstractmethod
def resize(self: TTruncable, significant: int) -> TTruncable:
raise NotImplementedError
[docs] @abc.abstractmethod
def truncate(self: TTruncable, significant: Optional[int] = None) -> TTruncable:
raise NotImplementedError
[docs] @abc.abstractmethod
def ceil(self: TTruncable, significant: Optional[int] = None) -> TTruncable:
raise NotImplementedError
[docs] @abc.abstractmethod
def floor(self: TTruncable, significant: Optional[int] = None) -> TTruncable:
raise NotImplementedError
@abc.abstractmethod
def __round__(self: TTruncable, significant: Optional[int] = None) -> TTruncable:
raise NotImplementedError
[docs]class PreciseNumber(Number, Truncable):
"""Abstract class of numbers with `PrecisionContext` compatibility"""
@property
@abc.abstractmethod
def significant(self) -> int:
raise NotImplementedError
def _get_significant(self: TPreciseNumber, other: "PreciseNumber") -> int:
return get_context()._precisionfunc(self, other)
@_with_context_precision(symbol="+")
def __add__(self: TPreciseNumber, other):
if f := get_context().add:
with set_precision(**asdict(PrecisionContext())):
return f(self, other)
return self._add(other)
@abc.abstractmethod
def _add(self: TPreciseNumber, other: "PreciseNumber") -> TPreciseNumber:
raise NotImplementedError
@_with_context_precision(symbol="-")
def __sub__(self: TPreciseNumber, other):
if f := get_context().sub:
with set_precision(**asdict(PrecisionContext())):
return f(self, other)
return self._sub(other)
@abc.abstractmethod
def _sub(self: TPreciseNumber, other: "PreciseNumber") -> TPreciseNumber:
raise NotImplementedError
@_with_context_precision(symbol="*")
def __mul__(self: TPreciseNumber, other):
if f := get_context().mul:
with set_precision(**asdict(PrecisionContext())):
return f(self, other)
return self._mul(other)
@abc.abstractmethod
def _mul(self: TPreciseNumber, other: "PreciseNumber") -> TPreciseNumber:
raise NotImplementedError
@_with_context_precision(symbol="/")
def __truediv__(self: TPreciseNumber, other):
if f := get_context().div:
with set_precision(**asdict(PrecisionContext())):
return f(self, other)
return self._truediv(other)
@abc.abstractmethod
def _truediv(self: TPreciseNumber, other: "PreciseNumber") -> TPreciseNumber:
raise NotImplementedError
@abc.abstractmethod
def __float__(self) -> float:
raise NotImplementedError
class FuncEnum(Enum):
def __call__(self, *args, **kwds) -> PreciseNumber:
return self.value[0](*args, **kwds)
[docs]class PrecisionMode(FuncEnum):
"""Enumeration of standard precision modes available.
You can also use a positive integer to indicate a precision at a
constant significant number.
"""
SCI = (
lambda x, y: min(x.significant, y.significant),
0,
) #: Following scientific notation
MAX = (lambda x, y: max(x.significant, y.significant), 1) #: Using max significant
FULL = (
lambda *_: (_ for _ in ()).throw(NotImplementedError),
2,
) #: TODO Full calculation
[docs]class TruncatureMode(FuncEnum):
"""Enumeration of standard truncature modes available."""
NONE = (lambda x: x, 0) #: No truncature
ROUND = (round, 1) #: round()
TRUNC = (lambda x: x.truncate(), 2) #: truncate()
CEIL = (lambda x: x.ceil(), 3) #: ceil()
FLOOR = (lambda x: x.floor(), 4) #: floor()
CustomArithmeticAlgorithm = Optional[
Callable[[PreciseNumber, PreciseNumber], PreciseNumber]
]
_AI_REGISTRY: Dict[CustomArithmeticAlgorithm, str] = {None: "DEFAULT"}
def identify_func(identifier: str):
"""Identify a custom arithmetic algorithm with a unique name"""
def wrapper(fn: Callable) -> CustomArithmeticAlgorithm:
if identifier in _AI_REGISTRY.values():
raise ValueError("Identifier already in use")
if fn in _AI_REGISTRY:
raise ValueError(
f"Function already registered with identifier {_AI_REGISTRY[fn]}"
)
_AI_REGISTRY[fn] = identifier
return fn
return wrapper
def remove_func(func: CustomArithmeticAlgorithm):
"""Remove a custom arithmetic algorithm from the registry"""
if func in _AI_REGISTRY:
del _AI_REGISTRY[func]
return True
return False
def find_func(identifier: str):
"""Find the function of an identifier that has been registered"""
return [k for k, v in _AI_REGISTRY.items() if v == identifier][0]
[docs]@dataclass
class PrecisionContext:
"""Context containing `PreciseNumber` arithmetic rules."""
#: Precision mode
pmode: PrecisionMode = PrecisionMode.MAX
#: Truncature mode
tmode: TruncatureMode = TruncatureMode.NONE
#: Addition `CustomArithmeticAlgorithm`
add: CustomArithmeticAlgorithm = None
#: Substraction `CustomArithmeticAlgorithm`
sub: CustomArithmeticAlgorithm = None
#: Multiplication `CustomArithmeticAlgorithm`
mul: CustomArithmeticAlgorithm = None
#: Division `CustomArithmeticAlgorithm`
div: CustomArithmeticAlgorithm = None
#: Recording mode
recording: bool = False
#: `set_precision` context stack
stack: int = field(init=False, default=0)
_records: List = field(init=False, default_factory=list)
def __post_init__(self):
if not isinstance(self.tmode, TruncatureMode):
raise TypeError
if isinstance(self.pmode, int):
if self.pmode < 0:
raise ValueError("Precision cannot be negative")
self._precisionfunc = lambda *_: self.pmode
elif isinstance(self.pmode, PrecisionMode):
self._precisionfunc = self.pmode
else:
raise TypeError
[docs] def mutate(
self,
pmode: Optional[PrecisionMode] = None,
tmode: Optional[TruncatureMode] = None,
recording: Optional[bool] = None,
add: Union[CustomArithmeticAlgorithm, Literal[False]] = False,
sub: Union[CustomArithmeticAlgorithm, Literal[False]] = False,
mul: Union[CustomArithmeticAlgorithm, Literal[False]] = False,
div: Union[CustomArithmeticAlgorithm, Literal[False]] = False,
):
"""Mutates this `PrecisionContext` with new rules."""
for f in [add, sub, mul, div]:
if f is not False and f not in _AI_REGISTRY:
raise ValueError("CustomArithmeticFunction not registered")
self.pmode = self.pmode if pmode is None else pmode
self.tmode = tmode or self.tmode
self.recording = self.recording if recording is None else recording
self.add = add if add is not False else self.add
self.sub = sub if sub is not False else self.sub
self.mul = mul if mul is not False else self.mul
self.div = div if div is not False else self.div
self.__post_init__()
[docs] def freeze(self):
"""Returns a `Dict` containing this context rules"""
return {
"tmode": self.tmode.name,
"pmode": self.pmode.name
if isinstance(self.pmode, PrecisionMode)
else self.pmode,
"add": _AI_REGISTRY[self.add],
"sub": _AI_REGISTRY[self.sub],
"mul": _AI_REGISTRY[self.mul],
"div": _AI_REGISTRY[self.div],
}
[docs] def record(self, *args):
"""Record an operation"""
if self.recording:
self._records.append({"args": args, **self.freeze()})
__CONTEXT = PrecisionContext()
[docs]def get_context() -> PrecisionContext:
"""Returns current context"""
return __CONTEXT
[docs]def set_context(context: PrecisionContext):
"""Replace current `PrecisionContext`.
:raises ValueError: Raise if you set a new context while inside a `set_precision` \
context manager.
"""
if get_context().stack > 0:
raise ValueError("You can't change context while inside a precision_context")
context.stack = 0
global __CONTEXT
__CONTEXT = context
[docs]def set_recording(flag: bool):
"""Set current `PrecisionContext` recording mode to `flag`.
:raises ValueError: Raise if you set recording while inside a `set_precision` \
context manager
"""
ctx = get_context()
if ctx.stack > 0:
raise ValueError(
"You can't start recording while inside a precision_context,\
you should use recording=True instead"
)
ctx.recording = flag
[docs]def get_records():
"""Get current `PrecisionContext` records."""
return get_context()._records
[docs]def clear_records():
"""Clear current `PrecisionContext` records."""
get_context()._records.clear()
[docs]@contextmanager
def set_precision(
pmode: Optional[PrecisionMode] = None,
tmode: Optional[TruncatureMode] = None,
recording: Optional[bool] = None,
add: Union[CustomArithmeticAlgorithm, Literal[False]] = False,
sub: Union[CustomArithmeticAlgorithm, Literal[False]] = False,
mul: Union[CustomArithmeticAlgorithm, Literal[False]] = False,
div: Union[CustomArithmeticAlgorithm, Literal[False]] = False,
**kwargs,
):
"""Mutates the current `PrecisionContext` with the specified rules."""
ctx = get_context()
current = asdict(ctx)
del current["_records"]
del current["stack"]
try:
ctx.stack += 1
ctx.mutate(pmode, tmode, recording, add, sub, mul, div)
yield asdict(ctx)
finally:
ctx.mutate(**current)
ctx.stack -= 1
class PrecisionError(Exception):
pass