Make gf2()
cast value
s in non-strict
mode
- so far, `gf2()` runs in a `strict` mode by default => `gf2(42)` results in a `ValueError` - we adapt `gf2()` (a.k.a. `lalib.elements.galois.GF2Element`) such that it behaves more like the built-in `bool()` => `gf2(42)` returns `one`, like `bool(42)` returns `True` - further, the `GF2Element` class is adjusted such that `one` and `zero` assume their counterparts to be `1`-like or `0`-like objects during binary operations; other values result in an error => for example, `one + 1` works but `one + 42` does not => so, when used in binary operations, `one` and `zero` continue to cast their counterparts in `strict` mode - simplify the casting logic - make `value` a positional-only argument in `gf2()` (like most of the built-in constructors) - provide more usage examples in the docstrings clarifying when `strict` mode is used and when not - propagate the above changes to the test suite: + adapt test cases with regard to the `strict` mode logic + add new test cases to `assert` how `gf2()` can process `str`ings directly + rename two test cases involving `complex` numbers to mimic the naming from the newly added test cases
This commit is contained in:
parent
b2f6155872
commit
06c26192ad
5 changed files with 162 additions and 21 deletions
|
@ -8,15 +8,32 @@ Then, use them:
|
||||||
|
|
||||||
>>> one + zero
|
>>> one + zero
|
||||||
one
|
one
|
||||||
|
>>> one + one
|
||||||
|
zero
|
||||||
|
|
||||||
|
>>> type(one)
|
||||||
|
gf2
|
||||||
|
|
||||||
|
The `gf2` type is similar to the built-in `bool`.
|
||||||
|
To cast objects as `gf2` values:
|
||||||
|
|
||||||
>>> gf2(0)
|
>>> gf2(0)
|
||||||
zero
|
zero
|
||||||
|
>>> gf2(1)
|
||||||
|
one
|
||||||
|
|
||||||
>>> gf2(42)
|
>>> gf2(42)
|
||||||
|
one
|
||||||
|
>>> gf2(-42)
|
||||||
|
one
|
||||||
|
|
||||||
|
Yet, there is also a `strict` mode where values
|
||||||
|
not equal to `1` or `0` within a `threshold` are not accepted.
|
||||||
|
|
||||||
|
>>> gf2(42, strict=True)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
ValueError: ...
|
ValueError: ...
|
||||||
>>> gf2(42, strict=False)
|
|
||||||
one
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from lalib.elements import galois
|
from lalib.elements import galois
|
||||||
|
|
|
@ -21,6 +21,14 @@ zero
|
||||||
>>> 0 * zero
|
>>> 0 * zero
|
||||||
zero
|
zero
|
||||||
|
|
||||||
|
Yet, for numbers not equal to `1` or `0`, this does not work:
|
||||||
|
|
||||||
|
>>> one + 42
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: ...
|
||||||
|
|
||||||
|
|
||||||
Further usage explanations of `one` and `zero`
|
Further usage explanations of `one` and `zero`
|
||||||
can be found in the various docstrings of the `GF2Element` class.
|
can be found in the various docstrings of the `GF2Element` class.
|
||||||
|
|
||||||
|
@ -48,8 +56,8 @@ def _to_gf2(
|
||||||
value: complex, # `mypy` reads `complex | float | int`
|
value: complex, # `mypy` reads `complex | float | int`
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
strict: bool = True,
|
strict: bool,
|
||||||
threshold: float = config.THRESHOLD,
|
threshold: float,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Cast a number as a possible Galois field value: `1` or `0`.
|
"""Cast a number as a possible Galois field value: `1` or `0`.
|
||||||
|
|
||||||
|
@ -98,7 +106,7 @@ def _to_gf2(
|
||||||
msg = "`value` must be either `1`-like or `0`-like"
|
msg = "`value` must be either `1`-like or `0`-like"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
if abs(value) < threshold:
|
if abs(value) < threshold and not math.isinf(value):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
@ -129,8 +137,9 @@ class GF2Element(metaclass=GF2Meta):
|
||||||
def __new__(
|
def __new__(
|
||||||
cls: type[Self],
|
cls: type[Self],
|
||||||
value: object = None,
|
value: object = None,
|
||||||
|
/,
|
||||||
*,
|
*,
|
||||||
strict: bool = True,
|
strict: bool = False,
|
||||||
threshold: float = config.THRESHOLD,
|
threshold: float = config.THRESHOLD,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""See docstring for `.__init__()`."""
|
"""See docstring for `.__init__()`."""
|
||||||
|
@ -146,7 +155,7 @@ class GF2Element(metaclass=GF2Meta):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
msg = "Must create `one` and `zero` first (internal error)"
|
msg = "Must create `one` and `zero` first (internal error)"
|
||||||
raise RuntimeError(msg) from None
|
raise RuntimeError(msg) from None
|
||||||
else:
|
|
||||||
value = _to_gf2(value, strict=strict, threshold=threshold) # type: ignore[arg-type]
|
value = _to_gf2(value, strict=strict, threshold=threshold) # type: ignore[arg-type]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -308,11 +317,14 @@ class GF2Element(metaclass=GF2Meta):
|
||||||
True
|
True
|
||||||
>>> one != zero
|
>>> one != zero
|
||||||
True
|
True
|
||||||
|
|
||||||
>>> one == 1
|
>>> one == 1
|
||||||
True
|
True
|
||||||
|
>>> one == 42
|
||||||
|
False
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
other = GF2Element(other)
|
other = GF2Element(other, strict=True)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
else:
|
else:
|
||||||
|
@ -333,11 +345,19 @@ class GF2Element(metaclass=GF2Meta):
|
||||||
True
|
True
|
||||||
>>> one < one
|
>>> one < one
|
||||||
False
|
False
|
||||||
|
|
||||||
>>> 0 < one
|
>>> 0 < one
|
||||||
True
|
True
|
||||||
|
|
||||||
|
The `other` object must be either `1`-like or `0`-like:
|
||||||
|
|
||||||
|
>>> one < 42
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: ...
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
other = GF2Element(other)
|
other = GF2Element(other, strict=True)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -357,8 +377,16 @@ class GF2Element(metaclass=GF2Meta):
|
||||||
True
|
True
|
||||||
>>> one <= zero
|
>>> one <= zero
|
||||||
False
|
False
|
||||||
|
|
||||||
>>> zero <= 1
|
>>> zero <= 1
|
||||||
True
|
True
|
||||||
|
|
||||||
|
The `other` object must be either `1`-like or `0`-like:
|
||||||
|
|
||||||
|
>>> zero <= 42
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: ...
|
||||||
"""
|
"""
|
||||||
# The `numbers.Rational` abstract base class requires both
|
# The `numbers.Rational` abstract base class requires both
|
||||||
# `.__lt__()` and `.__le__()` to be present alongside
|
# `.__lt__()` and `.__le__()` to be present alongside
|
||||||
|
@ -377,7 +405,7 @@ class GF2Element(metaclass=GF2Meta):
|
||||||
along the way.
|
along the way.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
other = GF2Element(other)
|
other = GF2Element(other, strict=True)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -413,10 +441,18 @@ class GF2Element(metaclass=GF2Meta):
|
||||||
one
|
one
|
||||||
>>> zero - one
|
>>> zero - one
|
||||||
one
|
one
|
||||||
|
|
||||||
>>> zero + 0
|
>>> zero + 0
|
||||||
zero
|
zero
|
||||||
>>> 1 + one
|
>>> 1 + one
|
||||||
zero
|
zero
|
||||||
|
|
||||||
|
The `other` object must be either `1`-like or `0`-like:
|
||||||
|
|
||||||
|
>>> zero + 42
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: ...
|
||||||
"""
|
"""
|
||||||
return self._compute(other, lambda s, o: (s + o) % 2)
|
return self._compute(other, lambda s, o: (s + o) % 2)
|
||||||
|
|
||||||
|
@ -435,8 +471,16 @@ class GF2Element(metaclass=GF2Meta):
|
||||||
one
|
one
|
||||||
>>> zero * one
|
>>> zero * one
|
||||||
zero
|
zero
|
||||||
|
|
||||||
>>> 0 * one
|
>>> 0 * one
|
||||||
zero
|
zero
|
||||||
|
|
||||||
|
The `other` object must be either `1`-like or `0`-like:
|
||||||
|
|
||||||
|
>>> one * 42
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: ...
|
||||||
"""
|
"""
|
||||||
return self._compute(other, lambda s, o: s * o)
|
return self._compute(other, lambda s, o: s * o)
|
||||||
|
|
||||||
|
@ -453,12 +497,21 @@ class GF2Element(metaclass=GF2Meta):
|
||||||
one
|
one
|
||||||
>>> zero // one
|
>>> zero // one
|
||||||
zero
|
zero
|
||||||
|
|
||||||
>>> one / zero
|
>>> one / zero
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
ZeroDivisionError: ...
|
ZeroDivisionError: ...
|
||||||
|
|
||||||
>>> 1 // one
|
>>> 1 // one
|
||||||
one
|
one
|
||||||
|
|
||||||
|
The `other` object must be either `1`-like or `0`-like:
|
||||||
|
|
||||||
|
>>> 42 / one
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: ...
|
||||||
"""
|
"""
|
||||||
return self._compute(other, lambda s, o: s / o)
|
return self._compute(other, lambda s, o: s / o)
|
||||||
|
|
||||||
|
@ -484,12 +537,21 @@ class GF2Element(metaclass=GF2Meta):
|
||||||
zero
|
zero
|
||||||
>>> zero % one
|
>>> zero % one
|
||||||
zero
|
zero
|
||||||
|
|
||||||
>>> one % zero
|
>>> one % zero
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
ZeroDivisionError: ...
|
ZeroDivisionError: ...
|
||||||
|
|
||||||
>>> 1 % one
|
>>> 1 % one
|
||||||
zero
|
zero
|
||||||
|
|
||||||
|
The `other` object must be either `1`-like or `0`-like:
|
||||||
|
|
||||||
|
>>> 42 % one
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: ...
|
||||||
"""
|
"""
|
||||||
return self._compute(other, lambda s, o: s % o)
|
return self._compute(other, lambda s, o: s % o)
|
||||||
|
|
||||||
|
@ -513,8 +575,16 @@ class GF2Element(metaclass=GF2Meta):
|
||||||
zero
|
zero
|
||||||
>>> one ** zero
|
>>> one ** zero
|
||||||
one
|
one
|
||||||
|
|
||||||
>>> 1 ** one
|
>>> 1 ** one
|
||||||
one
|
one
|
||||||
|
|
||||||
|
The `other` object must be either `1`-like or `0`-like:
|
||||||
|
|
||||||
|
>>> 42 ** one
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: ...
|
||||||
"""
|
"""
|
||||||
return self._compute(other, lambda s, o: s**o)
|
return self._compute(other, lambda s, o: s**o)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import random
|
import random
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from lalib import config
|
||||||
from lalib.elements import galois as gf2_elements
|
from lalib.elements import galois as gf2_elements
|
||||||
from lalib.fields import base
|
from lalib.fields import base
|
||||||
from lalib.fields import utils
|
from lalib.fields import utils
|
||||||
|
@ -13,6 +14,18 @@ class GaloisField2(utils.SingletonMixin, base.Field):
|
||||||
|
|
||||||
_math_name = "GF2"
|
_math_name = "GF2"
|
||||||
_dtype = gf2_elements.GF2Element
|
_dtype = gf2_elements.GF2Element
|
||||||
|
|
||||||
|
def _cast_func(
|
||||||
|
self,
|
||||||
|
value: Any,
|
||||||
|
/,
|
||||||
|
*,
|
||||||
|
strict: bool = True,
|
||||||
|
threshold: float = config.THRESHOLD,
|
||||||
|
**_kwargs: Any,
|
||||||
|
) -> gf2_elements.GF2Element:
|
||||||
|
return gf2_elements.gf2(value, strict=strict, threshold=threshold)
|
||||||
|
|
||||||
_additive_identity = gf2_elements.zero
|
_additive_identity = gf2_elements.zero
|
||||||
_multiplicative_identity = gf2_elements.one
|
_multiplicative_identity = gf2_elements.one
|
||||||
|
|
||||||
|
|
|
@ -115,16 +115,16 @@ class TestGF2Casting:
|
||||||
@pytest.mark.parametrize("value", one_like_values)
|
@pytest.mark.parametrize("value", one_like_values)
|
||||||
def test_cast_ones_not_strictly(self, cls, value):
|
def test_cast_ones_not_strictly(self, cls, value):
|
||||||
"""`gf2(value, strict=False)` returns `one`."""
|
"""`gf2(value, strict=False)` returns `one`."""
|
||||||
result = cls(value, strict=False)
|
result1 = cls(value)
|
||||||
assert result is one
|
assert result1 is one
|
||||||
|
|
||||||
|
result2 = cls(value, strict=False)
|
||||||
|
assert result2 is one
|
||||||
|
|
||||||
@pytest.mark.overlapping_test
|
@pytest.mark.overlapping_test
|
||||||
@pytest.mark.parametrize("value", non_strict_one_like_values)
|
@pytest.mark.parametrize("value", non_strict_one_like_values)
|
||||||
def test_cannot_cast_ones_strictly(self, cls, value):
|
def test_cannot_cast_ones_strictly(self, cls, value):
|
||||||
"""`gf2(value, strict=False)` returns `1`."""
|
"""`gf2(value, strict=True)` needs strict `1`-like values."""
|
||||||
with pytest.raises(ValueError, match="`1`-like or `0`-like"):
|
|
||||||
cls(value)
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="`1`-like or `0`-like"):
|
with pytest.raises(ValueError, match="`1`-like or `0`-like"):
|
||||||
cls(value, strict=True)
|
cls(value, strict=True)
|
||||||
|
|
||||||
|
|
|
@ -33,14 +33,14 @@ class TestCastAndValidateFieldElements:
|
||||||
utils.is_field_element(GF2, value)
|
utils.is_field_element(GF2, value)
|
||||||
|
|
||||||
@pytest.mark.parametrize("pre_value", [1, 0])
|
@pytest.mark.parametrize("pre_value", [1, 0])
|
||||||
def test_complex_number_is_field_element(self, pre_value):
|
def test_one_or_zero_like_complex_number_is_field_element(self, pre_value):
|
||||||
"""By design, `GF2` can process `complex` numbers."""
|
"""`GF2` can process `complex` numbers."""
|
||||||
value = complex(pre_value, 0)
|
value = complex(pre_value, 0)
|
||||||
utils.is_field_element(GF2, value)
|
utils.is_field_element(GF2, value)
|
||||||
|
|
||||||
@pytest.mark.parametrize("pre_value", [+42, -42])
|
@pytest.mark.parametrize("pre_value", [+42, -42])
|
||||||
def test_complex_number_is_not_field_element(self, pre_value):
|
def test_non_one_or_zero_like_complex_number_is_not_field_element(self, pre_value):
|
||||||
"""By design, `GF2` can process `complex` numbers ...
|
"""`GF2` can process `complex` numbers ...
|
||||||
|
|
||||||
... but they must be `one`-like or `zero`-like
|
... but they must be `one`-like or `zero`-like
|
||||||
to become a `GF2` element.
|
to become a `GF2` element.
|
||||||
|
@ -48,6 +48,17 @@ class TestCastAndValidateFieldElements:
|
||||||
value = complex(pre_value, 0)
|
value = complex(pre_value, 0)
|
||||||
utils.is_not_field_element(GF2, value)
|
utils.is_not_field_element(GF2, value)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("pre_value", [+42, -42])
|
||||||
|
def test_non_one_or_zero_like_complex_number_is_field_element(self, pre_value):
|
||||||
|
"""`GF2` can process all `complex` numbers in non-`strict` mode."""
|
||||||
|
value = complex(pre_value, 0)
|
||||||
|
|
||||||
|
left = GF2.cast(value, strict=False)
|
||||||
|
right = bool(value)
|
||||||
|
|
||||||
|
assert left == right
|
||||||
|
assert GF2.validate(value, strict=False)
|
||||||
|
|
||||||
@pytest.mark.parametrize("pre_value", ["NaN", "+inf", "-inf"])
|
@pytest.mark.parametrize("pre_value", ["NaN", "+inf", "-inf"])
|
||||||
def test_non_finite_complex_number_is_not_field_element(self, pre_value):
|
def test_non_finite_complex_number_is_not_field_element(self, pre_value):
|
||||||
"""For now, we only allow finite numbers as field elements.
|
"""For now, we only allow finite numbers as field elements.
|
||||||
|
@ -58,6 +69,36 @@ class TestCastAndValidateFieldElements:
|
||||||
value = complex(pre_value)
|
value = complex(pre_value)
|
||||||
utils.is_not_field_element(GF2, value)
|
utils.is_not_field_element(GF2, value)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ["1", "0"])
|
||||||
|
def test_one_or_zero_like_numeric_str_is_field_element(self, value):
|
||||||
|
"""`GF2` can process `str`ings resemling `1`s and `0`s."""
|
||||||
|
utils.is_field_element(GF2, value)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ["+42", "-42"])
|
||||||
|
def test_non_one_or_zero_like_numeric_str_is_not_field_element(self, value):
|
||||||
|
"""`GF2` can process `str`ings resembling numbers ...
|
||||||
|
|
||||||
|
... but they must be `1`-like or `0`-like.
|
||||||
|
"""
|
||||||
|
utils.is_not_field_element(GF2, value)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ["+42", "-42"])
|
||||||
|
def test_non_one_or_zero_like_numeric_str_is_field_element(self, value):
|
||||||
|
"""`GF2` can process `str`ings resemling any number in non-`strict` mode."""
|
||||||
|
left = GF2.cast(value, strict=False)
|
||||||
|
right = bool(float(value))
|
||||||
|
|
||||||
|
assert left == right
|
||||||
|
assert GF2.validate(value, strict=False)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ["NaN", "+inf", "-inf"])
|
||||||
|
def test_non_finite_numeric_str_is_not_field_element(self, value):
|
||||||
|
"""`GF2` can process `str`ings resemling numbers ...
|
||||||
|
|
||||||
|
... but they must represent finite numbers.
|
||||||
|
"""
|
||||||
|
utils.is_not_field_element(GF2, value)
|
||||||
|
|
||||||
|
|
||||||
class TestIsZero:
|
class TestIsZero:
|
||||||
"""Test specifics for `GF2.zero` and `GF2.is_zero()`."""
|
"""Test specifics for `GF2.zero` and `GF2.is_zero()`."""
|
||||||
|
|
Loading…
Reference in a new issue