vc2_bit_widths.infinite_arrays
: Infinite arrays¶
InfiniteArray
and its subclasses provide a way to define and
analyse VC-2 filters.
Tutorial¶
In the worked example below we’ll see how a simple LeGall (5, 3) synthesis
filter can be described using InfiniteArray
s. We’ll use this
description to enumerate all of the resulting filter phases and generate
algebraic expressions for them.
Building a VC-2 filter¶
A horizontal-only LeGall (5, 3) synthesis transform takes as input two arrays
of transform coefficients (a low-pass band, L
, and high-pass band, H
)
and produces a single output array. Lets start by defining the two input arrays:
>>> from vc2_bit_widths.infinite_arrays import SymbolArray
>>> L = SymbolArray(2, "L")
>>> H = SymbolArray(2, "H")
The SymbolArray
class defines an infinite array of
LinExp
s containing a single symbol of the
form LinExp((prefix, x, y))
. Like all InfiniteArray
subclasses,
we can access entries in our arrays using the usual Python subscript syntax:
>>> L[3, 4]
LinExp(('L', 3, 4))
>>> H[7, -5]
LinExp(('H', 7, -5))
Notice that the coordinate system extends to positive and negative infinity.
Next we’ll create an InterleavedArray
which contains a horizontal
interleaving of the two inputs:
>>> from vc2_bit_widths.infinite_arrays import InterleavedArray
>>> interleaved = InterleavedArray(L, H, 0)
The new interleaved array can be accessed just as before:
>>> interleaved[0, 0]
LinExp(('L', 0, 0))
>>> interleaved[1, 0]
LinExp(('H', 0, 0))
>>> interleaved[2, 1]
LinExp(('L', 1, 1))
>>> interleaved[3, 1]
LinExp(('H', 1, 1))
Next we’ll apply the two lifting stages which implement the LeGall (5, 3)
transform using a LiftedArray
:
>>> from vc2_bit_widths.infinite_arrays import LiftedArray
>>> from vc2_data_tables import LIFTING_FILTERS, WaveletFilters
>>> wavelet = LIFTING_FILTERS[WaveletFilters.le_gall_5_3]
>>> stage_0, stage_1 = wavelet.stages
>>> lifted_once = LiftedArray(interleaved, stage_0, 0)
>>> lifted_twice = LiftedArray(lifted_once, stage_1, 0)
Finally we’ll use a RightShiftedArray
to apply the final bit shift
operation to the result:
>>> from vc2_bit_widths.infinite_arrays import RightShiftedArray
>>> output = RightShiftedArray(lifted_twice, wavelet.filter_bit_shift)
This final array now, in effect, contains a complete algebraic description of
how each entry is computed in terms of our two input arrays, L
and H
:
>>> output[0, 0]
LinExp({('L', 0, 0): Fraction(1, 2), ('H', -1, 0): Fraction(-1, 8), ('H', 0, 0): Fraction(-1, 8), AAError(id=1): Fraction(-1, 4), AAError(id=2): Fraction(1, 2)})
Notice that the expression above refers to transform coefficients with negative
coordinates (e.g. ('H', -1, 0)
). This is because the infinite dimensions of
InfiniteArray
s allows LiftedArray
to ignore VC-2’s
filter edge effect behaviour. This makes it easier to analyse filter behaviours
without having to carefully avoid edge effected behaviour.
Filter phases¶
The InfiniteArray
subclasses automatically keep track of the period
of each array (see Terminology) in the InfiniteArray.period
property:
>>> L.period
(1, 1)
>>> H.period
(1, 1)
>>> output.period
(2, 1)
This makes it easy to automatically obtain an example of each filter phase in our output:
>>> all_output_filter_phases = [
... output[x, y]
... for x in range(output.period[0])
... for y in range(output.period[1])
... ]
Detecting no-operations¶
When performing a computationally expensive analysis on a set of filters
described by InfiniteArray
s it can be helpful to skip arrays
which just consist of interleavings and other non-transformative views of other
arrays. For this purpose, the InfiniteArray.nop
property indicates
if a given array implements a no-operation (nop) and therefore may be skipped.
From the example above:
>>> L.nop
False
>>> H.nop
False
>>> interleaved.nop
True
>>> lifted_once.nop
False
>>> lifted_twice.nop
False
>>> output.nop
False
Here we can see that the interleaving stage is identified as having no material effect on the values it contains.
Relative array stepping¶
InfiniteArray
s provide a
InfiniteArray.relative_step_size_to()
method for calculating the
scaling relationships between arrays. This can be useful for finding array
values which don’t use inputs with negative coordinates (i.e. would not be
affected by edge-effect behaviour in a real VC-2 filter).
For example, consider the top-left pixel of the filter output from before:
>>> output[0, 0]
LinExp({('L', 0, 0): Fraction(1, 2), ('H', -1, 0): Fraction(-1, 8), ('H', 0, 0): Fraction(-1, 8), AAError(id=1): Fraction(-1, 4), AAError(id=2): Fraction(1, 2)})
This uses the input ('H', -1, 0)
which has negative coordinates. Thanks to
the period
property we know that any array value with
an even-numbered X coordinate implements the same filter phase as [0, 0].
We could start trying every even-numbered X coordinate until we find an entry
without a negative H
coordinate, but this would be fairly inefficient.
Instead lets use relative_step_size_to()
to work out
how many steps we must move in output
to move all our H
coordinates
right by one step.
>>> output.relative_step_size_to(H)
(Fraction(1, 2), Fraction(1, 1))
This tells us that for every step we move in the X-dimension of output
, we
move half a step in that direction in H
. Therefore, we must pick a
coordinate at least two steps to the right in output
to avoid the -1
coordinate in H
. Moving to [2, 0] also gives an even
numbered X coordinate so we know that we’re going to see the same filter phase
and so we get:
>>> output[2, 0]
LinExp({('L', 1, 0): Fraction(1, 2), ('H', 0, 0): Fraction(-1, 8), ('H', 1, 0): Fraction(-1, 8), AAError(id=3): Fraction(-1, 4), AAError(id=6): Fraction(1, 2)})
Which implements the same filter phase as output[0, 0]
but without any
negative coordinates.
Caching¶
All of the subclasses of InfiniteArray
which perform a non-trivial
operation internally cache array values when they’re accessed.
The most obvious benefit of this behaviour is to impart a substantial performance improvement for larger filter designs.
A secondary benefit applies to InfiniteArray
s containing
LinExp
s. By caching the results of
operations which introduce affine arithmetic error symbols
(AAError
), these errors can correctly
combine or cancel when that result is reused. As a result, while caching is not
essential for correctness, it can materially improve the tightness of the
bounds produced.
In some instances, this basic caching behaviour may not go far enough. For
example, the contents of output[0, 0]
and output[2, 0]
are extremely
similar, consisting of essentially the same coefficients, just translated to
the right:
>>> output[0, 0]
LinExp({('L', 0, 0): Fraction(1, 2), ('H', -1, 0): Fraction(-1, 8), ('H', 0, 0): Fraction(-1, 8), AAError(id=1): Fraction(-1, 4), AAError(id=2): Fraction(1, 2)})
>>> output[2, 0]
LinExp({('L', 1, 0): Fraction(1, 2), ('H', 0, 0): Fraction(-1, 8), ('H', 1, 0): Fraction(-1, 8), AAError(id=3): Fraction(-1, 4), AAError(id=6): Fraction(1, 2)})
In principle, the cached result of output[0, 0]
could be re-used (and the
coefficients suitably translated) to save the computational cost of evaluating
output[2, 0]
from scratch. For extremely large transforms, this
optimisation can result in substantial savings in runtime and RAM. For use in
such scenarios the SymbolicPeriodicCachingArray
array type is provided:
>>> from vc2_bit_widths.infinite_arrays import SymbolicPeriodicCachingArray
>>> output_cached = SymbolicPeriodicCachingArray(output, L, H)
The constructor takes the array to be cached along with all
SymbolArray
s used its definition. The new array may be accessed
as usual but with more aggressive caching taking place internally:
>>> output_cached[0, 0]
LinExp({('L', 0, 0): Fraction(1, 2), ('H', -1, 0): Fraction(-1, 8), ('H', 0, 0): Fraction(-1, 8), AAError(id=1): Fraction(-1, 4), AAError(id=2): Fraction(1, 2)})
>>> output_cached[2, 0]
LinExp({('L', 1, 0): Fraction(1, 2), ('H', 0, 0): Fraction(-1, 8), ('H', 1, 0): Fraction(-1, 8), AAError(id=1): Fraction(-1, 4), AAError(id=2): Fraction(1, 2)})
Warning
Note that in the cached version of the array, the
AAError
terms are not unique between
output_cached[0, 0]
and output_cached[2, 0]
, though they should be.
This is a result of the caching mechanism having no way to know how error
terms should change between entries in the array. As a result,
SymbolicPeriodicCachingArray
must not be used when the affine
error terms in its output are expected to be significant.
Note
SymbolicPeriodicCachingArray
only works with
InfiniteArray
s defined in terms of SymbolArray
s.
API¶
InfiniteArray
(base class)¶
-
class
InfiniteArray
(ndim, cache)¶ An abstract base class describing an immutable infinite N-dimensional array of symbolic values.
Subclasses should implement
get()
to return the value at a given position in an array.Instances of this type may be indexed like an N-dimensional array.
The ‘cache’ argument controls whether array values are cached or not. It is recommended that this argument be set to ‘True’ for expensive to compute functions or functions which introduce new error terms (to ensure error terms are re-used)
-
get
(keys)¶ Called by
__getitem__()
with a tuple of array indices.The array of keys is guaranteed to be a tuple with
ndim
entries.Values returned by this method will be memoized (cached) by
__getitem__()
. Consequently, this method will only be called the first time a particular array index is requested. Subsequent accesses will return the cached value.
-
clear_cache
()¶ Clear the cache (if enabled).
-
property
ndim
¶ The number of dimensions in the array.
-
property
period
¶ Return the period of this array (see Terminology).
- Returns
- period(int, …)
The period of the array in each dimension.
-
property
nop
¶ True if this
InfiniteArray
is a no-operation (nop), i.e. it just views values in other arrays. False if some computation is performed.
-
relative_step_size_to
(other)¶ For a step along a dimension in this array, compute the equivalent step size in the provided array.
- Parameters
- other
InfiniteArray
An array to compare the step size with. Must have been used (maybe indirectly) to define this array.
- other
- Returns
- relative_step_size(
fractions.Fraction
, …) or None The relative step sizes for each dimension, or None if the provided array was not used in the computation of this array.
- relative_step_size(
-
Base value type arrays¶
-
class
SymbolArray
(ndim, prefix='v')¶ An infinite array of
LinExp
symbols.Symbols will be identified by tuples like
(prefix, n)
for a one dimensional array,(prefix, n, n)
for a two-dimensional array,(prefix, n, n, n)
for a three-dimensional array and so-on.Example usage:
>>> a = SymbolArray(3, "foo") >>> a[1, 2, 3] LinExp(('foo', 1, 2, 3)) >>> a[100, -5, 0] LinExp(('foo', 100, -5, 0))
- Parameters
- ndimint
The number of dimensions in the array.
- prefixobject
A prefix to be used as the first element of every symbol tuple.
-
class
VariableArray
(ndim, exp)¶ An infinite array of subscripted
PyExp
expressions.Example usage:
>>> from vc2_bit_widths.pyexp import Argument >>> arg = Argument("arg") >>> a = VariableArray(2, arg) >>> a[1, 2] Subscript(Argument('arg'), Constant((1, 2))) >>> a[-10, 3] Subscript(Argument('arg'), Constant((-10, 3)))
- Parameters
- ndimint
The number of dimensions in the array.
- exp
PyExp
The expression to be subscripted in each array element.
Computation arrays¶
-
class
LiftedArray
(input_array, stage, filter_dimension)¶ Apply a one-dimensional lifting filter step to an array, as described in the VC-2 specification (15.4.4).
- Parameters
- input_array
InfiniteArray
The input array whose entries will be filtered by the specified lifting stage.
- stage
vc2_data_tables.LiftingStage
A description of the lifting stage.
- interleave_dimension: int
The dimension along which the filter will act.
- input_array
-
class
LeftShiftedArray
(input_array, shift_bits=1)¶ Apply a left bit shift to every value in an input array.
- Parameters
- input_array
InfiniteArray
The array to have its values left-shifted.
- shift_bits: int
Number of bits to shift by.
- input_array
-
class
RightShiftedArray
(input_array, shift_bits=1)¶ Apply a right bit shift to every value in an input array.
The right-shift operation is based on the description in the VC-2 specification (15.4.3). Specifically, \(2^{\text{shiftbits}-1}\) is added to the input values prior to the right-shift operation (which is used to implement rounding behaviour in integer arithmetic).
- Parameters
- input_array
InfiniteArray
The array to have its values right-sifted
- shift_bits: int
Number of bits to shift by
- input_array
Sampling arrays¶
-
class
SubsampledArray
(input_array, steps, offsets)¶ A subsampled view of another
InfiniteArray
.- Parameters
- input_array
InfiniteArray
The array to be subsampled.
- steps, offsets: (int, …)
Tuples giving the step size and start offset for each dimension.
When this array is indexed, the index into the input array is computed as:
input_array_index[n] = (index[n] * step[n]) + offset[n]
- input_array
-
class
InterleavedArray
(array_even, array_odd, interleave_dimension)¶ An array view which interleaves two
InfiniteArray
s together into a single array.- Parameters
- array_even, array_odd
InfiniteArray
The two arrays to be interleaved. ‘array_even’ will be used for even-indexed values in the specified dimension and ‘array_odd’ for odd.
- interleave_dimension: int
The dimension along which the two arrays will be interleaved.
- array_even, array_odd
Caching arrays¶
-
class
SymbolicPeriodicCachingArray
(array, *symbol_arrays)¶ A caching view of a
InfiniteArray
ofLinExp
values.This view will request at most one value from each filter phase of the input array and compute all other values by altering the indices in the previously computed values.
For example, normally, when accessing values within a
InfiniteArray
, values are computed from scratch on-demand:>>> s = SymbolArray(2, "s") >>> stage = <some filter stage> >>> la = LiftedArray(s, stage, 0) >>> la[0, 0] # la[0, 0] worked out from scratch LinExp(...) >>> la[0, 1] # la[0, 1] worked out from scratch LinExp(...) >>> la[0, 2] # la[0, 2] worked out from scratch LinExp(...) >>> la[0, 3] # la[0, 3] worked out from scratch LinExp(...)
By contrast, when values are accessed in a
SymbolicPeriodicCachingArray
, only the first access to each filter phase is computed from scratch. All other values are found by changing the symbol indices in the cached array value:>>> cached_la = SymbolicPeriodicCachingArray(la, s) >>> cached_la[0, 0] # la[0, 0] worked out from scratch LinExp(...) >>> cached_la[0, 1] # la[0, 1] worked out from scratch LinExp(...) >>> cached_la[0, 2] # Cached value of la[0, 0] reused and mutated LinExp(...) >>> cached_la[0, 3] # Cached value of la[0, 1] reused and mutated LinExp(...)
- Parameters
- array
InfiniteArray
The array whose values are to be cached. These array values must consist only of symbols from
SymbolArray
s passed as arguments,AAError
terms and constants.Warning
Error terms may be repeated as returned from this array where they would usually be unique. If this is a problem, you should not use this caching view.
For example:
>>> s = SymbolArray(2, "s") >>> sa = RightShiftedArray(s, 1) >>> # Unique AAError terms (without caching) >>> sa[0, 0] LinExp({('s', 0, 0): Fraction(1, 2), AAError(id=1): Fraction(1, 2)}) >>> sa[0, 1] LinExp({('s', 0, 1): Fraction(1, 2), AAError(id=2): Fraction(1, 2)}) >>> # Non-unique AAError terms after caching >>> ca = SymbolicPeriodicCachingArray(sa, s) >>> ca[0, 0] LinExp({('s', 0, 0): Fraction(1, 2), AAError(id=1): Fraction(1, 2)}) >>> ca[0, 1] LinExp({('s', 0, 1): Fraction(1, 2), AAError(id=1): Fraction(1, 2)})
- symbol_arrays
SymbolArray
The symbol arrays which the ‘array’ argument is constructed from.
- array