Matrix Slicing¶
Affine matrix expressions form the core of PICOS’ modeling toolbox: All
constant
and
variable
expressions that you enter,
including integral variables, and any linear combination of these objects, are
stored as instances of the multidimensional
ComplexAffineExpression
or its real subclass
AffineExpression
.
Their common base class BiaffineExpression
implements plenty of algebraic operations to combine and modify your initial
expressions to yield the desired statements. One of these operations is slicing,
denoted by A[·]
for an affine expression A
.
Preliminaries
Unlike in NumPy, all multidimensional expressions are strictly matrices.
In particular, there are no flat arrays but only row and column vectors, and any
scalar expression is also a matrix.
PICOS does not support tensors or higher order expressions, but it does support
the
Kronecker product as well
as
partial trace
and
partial transposition
operations to enable some of the optimization problems naturally defined on
tensors.
If you enter data in the form of a flat array (e.g. a Python list
or
a NumPy ndarray
with one axis), it will be read as a
column vector.
In PICOS, all indices start from zero.
Slicing modes
PICOS has two modes for slicing: Arbitrary Access and Proper Slicing.
Arbitrary Access lets you select individual elements from a vector or matrix
expression and put them in a column vector in the desired order.
Transposition
,
reshaping
and
broadcasting
can then
be used to put the selection into the desired shape.
Arbitrary Access has the form A[·]
where ·
stands for an integer, a
Python slice
, a flat collection of integers such as a list
,
or a dictionary storing sparse index pairs.
Proper Slicing refers to selecting certain rows and columns of a matrix, and
forming a new matrix where all elements that are not selected are removed.
It has the form A[·,·]
where each ·
stands for an integer, a
slice
, or a flat collection of integers.
To demonstrate the different possibilities, we use a constant
expression:
>>> from picos import Constant
>>> A = Constant("A", range(25), (5,5))
>>> A
<5×5 Real Constant: A>
>>> print(A)
[ 0.00e+00 5.00e+00 1.00e+01 1.50e+01 2.00e+01]
[ 1.00e+00 6.00e+00 1.10e+01 1.60e+01 2.10e+01]
[ 2.00e+00 7.00e+00 1.20e+01 1.70e+01 2.20e+01]
[ 3.00e+00 8.00e+00 1.30e+01 1.80e+01 2.30e+01]
[ 4.00e+00 9.00e+00 1.40e+01 1.90e+01 2.40e+01]
Arbitrary Access¶
By integer
If a single integer or a single flat collection of integers is given, then these
indices refer to the column-major vectorization of the matrix, represented by
the order of the numbers in the demonstration matrix A
.
The most common case is selecting a single element via an integer index:
>>> A[0] # Select the first element as a scalar expression.
<1×1 Real Constant: A[0]>
>>> print(A[0]) # Print its value.
0.0
>>> print(A[7]) # The value of the eighth element.
7.0
>>> # Negative indices are counted from the rear; -1 refers to the last element:
>>> print(A[-1])
24.0
By slice
Python slices allow you to compactly specify a structured sequence of elements
to extract.
A Python slice has the form a:b
or a:b:s
with the inclusive
start index,
the exclusive stop index and
a step size.
Negative
and
, as in the integer index case, are counted from
the rear, while a negative step size reverses the order.
All of
,
and
may be omitted. Then, the defaults are
Note the None
in the statement above: When going backwards, this special
token is the only way to stop at the first element with index as
refers to the last element. For example, the first two elements in
reverse order are selected via the slice
1:None:-1
or just 1::-1
.
>>> A[:2] # The first two elements as a column vector.
<2×1 Real Constant: A[:2]>
>>> print(A[:2])
[ 0.00e+00]
[ 1.00e+00]
>>> print(A[1::-1]) # The first two elements reversed (indices 1 and 0).
[ 1.00e+00]
[ 0.00e+00]
>>> print(A[-2:]) # The last two elements.
[ 2.30e+01]
[ 2.40e+01]
>>> print(A[2:7].T) # The third to seventh element (transposed).
[ 2.00e+00 3.00e+00 4.00e+00 5.00e+00 6.00e+00]
>>> print(A[2:7:2].T) # As before, but with a step size of 2.
[ 2.00e+00 4.00e+00 6.00e+00]
You could use this to vectorize in column-major order, but
A.vec
is both individually faster and has its result cached:
>>> A[:].equals(A.vec)
True
>>> A.vec is A.vec # Cached.
True
>>> A[:] is A[:] # Computed again as new expression.
False
By integer sequence
By providing a list
or a similar vector of integers, you can select
arbitrary elements in any order, including duplicates:
>>> print(A[[0,1,0,1,-1]])
[ 0.00e+00]
[ 1.00e+00]
[ 0.00e+00]
[ 1.00e+00]
[ 2.40e+01]
Note that you cannot provide a tuple
instead of a list, as A[(·,·)]
is understood by Python as A[·,·]
(see Proper Slicing).
Any other object that the function load_data
with typecode="i"
loads as an integer row or column vector works, including
integral NumPy arrays.
By sparse index pair dictionary
If you provide a dictionary with exactly two keys that can be compared via
<
and whose values are integer sequences of same length (anything recognized
by load_data
as an integer vector), PICOS
interprets the sequence corresponding to the smaller key as row indices and the
sequence corresponding to the greater key as the corresponding column indices:
>>> print(A[{"x": range(3), "y": [1]*3}]) # Select (0,1), (1,1) and (2,1).
[ 5.00e+00]
[ 6.00e+00]
[ 7.00e+00]
>>> print(A[{"y": range(3), "x": [1]*3}]) # Transposed selection, as "x" < "y".
[ 1.00e+00]
[ 6.00e+00]
[ 1.10e+01]
You could use this to extract the main diagonal of , but
A.maindiag
is both individually faster and has its result cached:
>>> indices = dict(enumerate([range(min(A.shape))]*2))
>>> indices
{0: range(0, 5), 1: range(0, 5)}
>>> A[indices].equals(A.maindiag)
True
>>> A.maindiag is A.maindiag # Cached.
True
>>> A[indices] is A[indices] # Computed again as new expression.
False
Proper Slicing¶
If you provide not one but two integers, slices, or integer sequences separated
by a comma or given as a tuple
, then they are understood as row and
column indices, respectively.
Unlike when providing a sparse index pair by dictionary, these indices select
entire rows and columns and PICOS returns the matrix of all elements that are
selected twice (both by row and by column):
>>> print(A[1,2]) # The single element at (1,2) (second row, third column).
11.0
>>> print(A[0,:]) # The first row of the matrix.
[ 0.00e+00 5.00e+00 1.00e+01 1.50e+01 2.00e+01]
>>> print(A[range(3),-1]) # The first three elements of the last column.
[ 2.00e+01]
[ 2.10e+01]
[ 2.20e+01]
>>> print(A[[0,1],[0,1]]) # The first second-order principal submatrix.
[ 0.00e+00 5.00e+00]
[ 1.00e+00 6.00e+00]
>>> print(A[1:-1,1:-1]) # Cut away the outermost pixels of an image.
[ 6.00e+00 1.10e+01 1.60e+01]
[ 7.00e+00 1.20e+01 1.70e+01]
[ 8.00e+00 1.30e+01 1.80e+01]
>>> print(A[::2,::2]) # Sample every second element.
[ 0.00e+00 1.00e+01 2.00e+01]
[ 2.00e+00 1.20e+01 2.20e+01]
[ 4.00e+00 1.40e+01 2.40e+01]
You can even select the entire matrix to effectively create a copy of it, though this is discouraged as expressions are supposed to be immutable so that reusing an expression in multiple places is always safe.
>>> A[:,:].equals(A)
True
>>> A[:,:] is A
False
We refer to this as proper slicing because you cut out the rows that you want, throwing away the rest, then cut the desired columns out from the remainder. It’s like cutting a square cake except that you can also duplicate the pieces!
Note
In NumPy, A[[0,1],[0,1]]
would create a flat array with the elements
A[0,0]
and A[1,1]
while PICOS creates a submatrix from the first two
rows and columns as in the example above. If you want to mirror NumPy’s
behavior in PICOS, see By sparse index pair dictionary.