# Copyright 2021 National Technology & Engineering Solutions
# of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS,
# the U.S. Government retains certain rights in this software.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Functionality for encoding domain values using integer fields."""
import numbers
import numpy
[docs]
class Bits(object):
"""Converts arrays of bit values to and from field values.
"""
def __eq__(self, other):
return isinstance(other, Bits)
def __repr__(self):
return f"cicada.encoding.Bits()" # pragma: no cover
[docs]
def decode(self, array, field):
"""Convert an array of field values to bit values.
Parameters
----------
array: :class:`numpy.ndarray`, or :any:`None`, required
Array of field values containing only :math:`0` or :math:`1`.
field: :class:`cicada.arithmetic.Field`, required
Field over which the values in `array` are defined.
Returns
-------
decoded: :class:`numpy.ndarray` or :any:`None`
Array of integers containing the values :math:`0` and :math:`1`, or
:any:`None` if the input was :any:`None`.
Raises
------
ValueError
If `array` contains anything but :math:`0` or :math:`1`.
"""
if array is None:
return array
if not isinstance(array, numpy.ndarray):
raise ValueError(f"Expected array to be an instance of numpy.ndarray, got {type(array)} instead.") # pragma: no cover
if not array.dtype == field.dtype:
raise ValueError(f"Expected array dtype to be {field.dtype}, got {array.dtype} instead.") # pragma: no cover
# Strict enforcement - input must only contain zeros and ones.
result = array.astype(bool)
if not numpy.array_equal(array, result):
raise ValueError(f"Expected array to contain only zeros and ones, got {array} instead.") # pragma: no cover
return array.astype(numpy.uint8)
[docs]
def encode(self, array, field):
"""Convert an array of bit values to field values.
Parameters
----------
array: :class:`numpy.ndarray` or :any:`None`, required
Array to convert containing only :math:`0` or :math:`1`.
field: :class:`cicada.arithmetic.Field`, required
Field over which the returned values are defined.
Returns
-------
encoded: :class:`numpy.ndarray` or :any:`None`
Encoded array with the same shape as the input, containing the
`field` values :math:`0` and :math:`1`, or :any:`None` if the input
was :any:`None`.
Raises
------
ValueError
If `array` contains anything but :math:`0` or :math:`1`.
"""
if array is None:
return array
if not isinstance(array, numpy.ndarray):
raise ValueError(f"Expected array to be an instance of numpy.ndarray, got {type(array)} instead.") # pragma: no cover
# Strict enforcement - input must only contain zeros and ones.
result = array.astype(bool)
if not numpy.array_equal(array, result):
raise ValueError(f"Expected array to contain only zeros and ones, got {array} instead.") # pragma: no cover
# Convert to the field.
return field(result)
[docs]
class Boolean(object):
"""Converts arrays of boolean values to and from field values.
"""
def __eq__(self, other):
return isinstance(other, Boolean)
def __repr__(self):
return f"cicada.encoding.Boolean()" # pragma: no cover
[docs]
def decode(self, array, field):
"""Convert an array of field values to boolean values.
Parameters
----------
array: :class:`numpy.ndarray`, or :any:`None`, required
Array of field values containing only :math:`0` or :math:`1`.
field: :class:`cicada.arithmetic.Field`, required
Field over which the values in `array` are defined.
Returns
-------
decoded: :class:`numpy.ndarray` or :any:`None`
Array of boolean values :math:`True` and :math:`False`, or
:any:`None` if the input was :any:`None`.
"""
if array is None:
return array
if not isinstance(array, numpy.ndarray):
raise ValueError(f"Expected array to be an instance of numpy.ndarray, got {type(array)} instead.") # pragma: no cover
if not array.dtype == field.dtype:
raise ValueError(f"Expected array dtype to be {field.dtype}, got {array.dtype} instead.") # pragma: nocover
return array.astype(bool)
[docs]
def encode(self, array, field):
"""Convert an array of boolean values to field values.
Parameters
----------
array: :class:`numpy.ndarray` or :any:`None`, required
Array to convert. Nonzero values are considered :math:`True`, zero values are considered :math:`False`.
field: :class:`cicada.arithmetic.Field`, required
Field over which the returned values are defined.
Returns
-------
encoded: :class:`numpy.ndarray` or :any:`None`
Encoded array with the same shape as the input, containing the
`field` values :math:`0` and :math:`1`, or :any:`None` if the input
was :any:`None`.
"""
if array is None:
return array
if not isinstance(array, numpy.ndarray):
raise ValueError(f"Expected array to be an instance of numpy.ndarray, got {type(array)} instead.") # pragma: nocover
# Permissive coercion of truthy values.
result = array.astype(bool)
# Convert to the field.
return field(result)
[docs]
class FixedPoint(object):
"""Encodes real values in a field using a fixed-point representation.
Encoded values are :class:`numpy.ndarray` instances containing Python
integers, with `precision` bits reserved for encoding fractional digits.
Decoded values will be :class:`numpy.ndarray` instances containining 64-bit
floating point values.
Parameters
----------
precision: :class:`int`, optional
The number of bits reserved to store fractions in encoded values. Defaults
to 16.
"""
def __init__(self, precision=16):
if not isinstance(precision, numbers.Integral):
raise ValueError(f"Expected integer precision, got {type(precision)} instead.") # pragma: no cover
if precision < 0:
raise ValueError(f"Expected non-negative precision, got {precision} instead.") # pragma: no cover
self._precision = precision
self._scale = int(2**self._precision)
def __eq__(self, other):
return isinstance(other, FixedPoint) and self._precision == other._precision
def __repr__(self):
return f"cicada.encoding.FixedPoint(precision={self._precision})" # pragma: no cover
[docs]
def decode(self, array, field):
"""Convert an array of field values to an array of real values.
Parameters
----------
array: :class:`numpy.ndarray`, or :any:`None`, required
Array of field values created with :meth:`encode`.
field: :class:`cicada.arithmetic.Field`, required
Field used to create `array`.
Returns
-------
decoded: :class:`numpy.ndarray`
A floating point array with the same shape as the input, containing
the decoded representation of `array`, or :any:`None` if the input
was :any:`None`.
"""
if array is None:
return array
if not isinstance(array, numpy.ndarray):
raise ValueError(f"Expected array to be an instance of numpy.ndarray, got {type(array)} instead.") # pragma: nocover
if not array.dtype == field.dtype:
raise ValueError(f"Expected array dtype to be {field.dtype}, got {array.dtype} instead.") # pragma: nocover
order = field.order
posbound = order // 2
# Set aside storage for the result (ensures that we return an array and not a scalar).
output = numpy.empty_like(array, dtype=numpy.float64)
# Convert from the field to a plain array of integers.
result = numpy.copy(array, subok=False)
# Switch from twos-complement notation to negative values.
result = numpy.where(result > posbound, -(order - result), result)
# Shift values back to the right and convert to reals.
return numpy.divide(result.astype(numpy.float64), self._scale, out=output)
[docs]
def encode(self, array, field):
"""Convert array of real values to an array of field values using a fixed point integer representation.
Parameters
----------
array: :class:`numpy.ndarray` or :any:`None`, required
The array to convert.
field: :class:`cicada.arithmetic.Field`, required
The returned array elements will be members of this field.
Returns
-------
encoded: :class:`numpy.ndarray` or :any:`None`
Encoded array with the same shape as the input, containing the
fixed precision integer representation of `array`, or :any:`None`
if the input was :any:`None`.
"""
if array is None:
return array
if not isinstance(array, numpy.ndarray):
raise ValueError(f"Expected array to be an instance of numpy.ndarray, got {type(array)} instead.") # pragma: nocover
order = field.order
posbound = order // 2
# Ensure we have an array, but only copy data if necessary.
result = numpy.asarray(array, dtype=numpy.float64)
# Shift array values left. Don't do this inline!
result = result * self._scale
# Test to be sure our values are in-range for the field.
if numpy.any(numpy.abs(result) >= posbound):
raise ValueError("Values to be encoded are too large for representation in the field.") # pragma: no cover
# Convert to integers, using the Python modulo operator to handle negative values.
result = numpy.array([int(x) % order for x in numpy.nditer(result, order="C")], dtype=field.dtype).reshape(result.shape, order="C")
# Convert to a field.
return field(result)
@property
def precision(self):
return self._precision
[docs]
class Identity(object):
"""Encodes and decodes field values without modification.
Encoded values are :class:`numpy.ndarray` instances containing Python
integers. Decoded values will be the same.
"""
def __eq__(self, other):
return isinstance(other, Identity)
def __repr__(self):
return f"cicada.encoding.Identity()" # pragma: no cover
[docs]
def decode(self, array, field):
"""Return an array of field values without modification.
Parameters
----------
array: :class:`numpy.ndarray`, or :any:`None`, required
Array of field values created with :meth:`encode`.
Returns
-------
decoded: :class:`numpy.ndarray` or :any:`None`
An array of Python integers, or :any:`None` if the input was
:any:`None`.
"""
if array is None:
return array
if not isinstance(array, numpy.ndarray):
raise ValueError(f"Expected array to be an instance of numpy.ndarray, got {type(array)} instead.") # pragma: nocover
if not array.dtype == field.dtype:
raise ValueError(f"Expected array dtype to be {field.dtype}, got {array.dtype} instead.") # pragma: nocover
return array
[docs]
def encode(self, array, field):
"""Convert array of integer values to an array of field values without modification.
Parameters
----------
array: :class:`numpy.ndarray` or :any:`None`, required
The array to convert.
field: :class:`cicada.arithmetic.Field`, required
The returned array elements will be members of this field.
Returns
-------
encoded: :class:`numpy.ndarray` or :any:`None`
Encoded array with the same shape as the input, containing the
fixed precision integer representation of `array`, or :any:`None`
if the input was :any:`None`.
"""
if array is None:
return array
if not isinstance(array, numpy.ndarray):
raise ValueError(f"Expected array to be an instance of numpy.ndarray, got {type(array)} instead.") # pragma: nocover
# Convert to a field.
return field(array)