import numpy as np
import numpy.typing as npt
from enum import Enum, auto
class QFitMethod(Enum):
r"""An enum class used to specify how the maximum and minimum of the traces
is estimated based on a set of fitting traces.
With method = QuantFitMethod.BOUNDS they are estimated as the minimum and
maximum of the fitting trace respectively.
With method = QuantFitMethod.MOMENT they are estimated as the average of
the fitting traces minus/plus seven standard deviations
"""
BOUNDS = auto()
MOMENT = auto()
[docs]class QuantFitMethod:
"""Method for esimating the scale and shift parameters of Quantizer."""
[docs] @classmethod
def bounds(cls, margin=4.0):
"""Take the min and max of the training traces, fit such that the [min,
max] range is mapped to a zero-centered interval covering a ``1/margin``
fraction of the quantized domain: if the quantized domain is
``[-Q,Q]``, ``min` is mapped to ``-Q/margin`` and ``max`` is mapped to
``Q/margin``.
"""
return cls(QFitMethod.BOUNDS, margin=margin)
[docs] @classmethod
def moment(cls, nstd=7.0):
"""Take the mean and standard deviation of the training traces, fit
such that ``mean-nstd*std`` is mapped to `-Q` and ``mean+nstd*std`` is
mapped to `Q`, where `[-Q, Q]` is the quantized domain.
"""
return cls(QFitMethod.MOMENT, nstd=nstd)
def __init__(self, method: QFitMethod, **kwargs):
self.method = method
self.opts = kwargs
[docs]class Quantizer:
r"""Quantize a side channel traces given as an array of float into an array
of int16.
The quantizer estimates a shift and scale that minimize the loss due to the
rounding operation.
.. math::
\mathrm{Quantize}( x) = \mathrm{Round}((x - \mathrm{Shift}) \cdot \mathrm{Scale})
The shift and scale parameter can be provided explicitly, or can be
estimated based on a few traces.
Warning
^^^^^^^
The quantization procedure operates pointwise: each point is shifted and
scaled by a different value.
As a consequence the quantized version of the trace probably does not look
like its non quantized version.
Parameters
----------
shift : npt.NDArray[np.floating]
The value to shift every traces.
scale : npt.NDArray[np.floating]
The value to scale every traces.
Examples
--------
>>> from scalib.preprocessing import Quantizer
>>> import numpy as np
>>> # 500 traces of 200 points
>>> traces : npt.NDArray[np.floating] = np.random.randn(500,200)
>>> quantizer = Quantizer.fit(traces)
>>> quantized_traces : npt.NDArray[np.int16] = quantizer.quantize(traces)
>>> # Can be reused directly on 5000 new traces for instance
>>> traces : npt.NDArray[np.floating] = np.random.randn(5000,200)
>>> quantized_traces : npt.NDArray[np.int16] = quantizer.quantize(traces)
"""
def __init__(
self, shift: npt.NDArray[np.floating], scale: npt.NDArray[np.floating]
):
self._shift = shift
self._scale = scale
[docs] @classmethod
def fit(
cls,
traces: npt.NDArray[np.floating],
method: QuantFitMethod = QuantFitMethod.bounds(),
):
r"""Compute the shift and scale estimation from sample of `traces`
This class method returns an instance of Quantizer with the
corresponding shift and scale.
Parameters
----------
traces : array_like, np.floating
Array that contains the traces to estimate the shift and scale in
the quantization. The array must be of dimension ``(n, ns)``
method : QuantFitMethod
A member of QuantFitMethod enum class that specifies how the
minimum and maximum value of the trace to be quantized is
estimated.
"""
if method.method == QFitMethod.BOUNDS:
# Max/Min Centering and Multiplication by a constant prior to
# quantization to avoid information loss via rounding error
max = np.amax(traces, axis=0)
min = np.amin(traces, axis=0)
shift = (max + min) / 2
scale = 2**15 / ((max - min) / 2) / method.opts["margin"]
elif method.method == QFitMethod.MOMENT:
# Gaussian Methods
mean = np.mean(traces, axis=0)
std = np.std(traces, axis=0, ddof=1)
shift = mean
scale = 2**15 / (method.opts["nstd"] * std)
else:
raise ValueError("method.method should be a QFitMethod object")
return cls(shift, scale)
[docs] def quantize(
self, traces: npt.NDArray[np.floating], clip: bool = False
) -> npt.NDArray[np.int16]:
r"""Quantize the traces provide in `traces`
Parameters
----------
traces : array_like, np.floating
Array that contains the traces to be quantized into int16. The array must
be of dimension ``(n, ns)``
clip : bool
Boolean to bypass the overflow check prior to quantization and clip
the overflowing values to the boundaries.
By default it is set to False.
"""
adjusted_traces: npt.NDArray[np.floating] = (traces - self._shift) * self._scale
if clip:
adjusted_traces = np.clip(adjusted_traces, -(2**15), 2**15 - 1)
else:
overflow = (adjusted_traces > 2**15 - 1).any() or (
adjusted_traces < -(2**15)
).any()
if overflow:
raise ValueError(
"Overflow detected in the quantization. Update shift and "
"scale more precisely to avoid the error."
)
quantized_traces: npt.NDArray[np.int16] = adjusted_traces.astype(np.int16)
return quantized_traces