MATLAB: Sort a 3d array by another 3d array - arrays

I have a 3d array A of random numbers which I'd like to order each k'th dimension individually:
A=rand(3,1,16);
[m, n, k]=size(A);
The array that dictates the order of each matrix in the 3rd dimension is B:
B=randi(3,3,1,16); %this should be without replacement but think it will work anyway
If A(:,1,1)=[0.5, 0.2, 0.6]' and B(:,1,1)=[3,1,2] then the ordered A should be [0.2, 0.6, 0.5]' and so on for each A(:,1,1:k). Please note it's not ordering A numerically.
A(B) is what I might have expected to work and keeps the dimensions but not the right orders.
I've tried to work through this: https://uk.mathworks.com/matlabcentral/answers/307838-sort-3d-matrix-according-to-another-3d-matrix without any success.
Any thoughts would be much appreciated.

To sort A along the k-th dimension based on B: if your data is not complex, a simple way is:
Pack A and B into a complex array where A is the imaginary part and B is the real part;
Sort along the k-th dimension based on the real part;
Keep the imaginary part.
This is done in one line as follows:
result = imag(sort(B+1j*A, k, 'ComparisonMethod', 'real'));
Example:
>> A = rand(2,4,3)
A(:,:,1) =
0.162182308193243 0.311215042044805 0.165648729499781 0.262971284540144
0.794284540683907 0.528533135506213 0.601981941401637 0.654079098476782
A(:,:,2) =
0.689214503140008 0.450541598502498 0.228976968716819 0.152378018969223
0.748151592823709 0.083821377996933 0.913337361501670 0.825816977489547
A(:,:,3) =
0.538342435260057 0.078175528753184 0.106652770180584 0.004634224134067
0.996134716626885 0.442678269775446 0.961898080855054 0.774910464711502
>> B = randi(9, size(A))
B(:,:,1) =
8 1 3 4
8 4 8 9
B(:,:,2) =
2 2 8 5
3 2 6 2
B(:,:,3) =
8 4 4 3
6 5 1 2
>> k = 2;
>> result = imag(sort(B+1j*A, k, 'ComparisonMethod', 'real'))
result(:,:,1) =
0.311215042044805 0.165648729499781 0.262971284540144 0.162182308193243
0.528533135506213 0.601981941401637 0.794284540683907 0.654079098476782
result(:,:,2) =
0.450541598502498 0.689214503140008 0.152378018969223 0.228976968716819
0.083821377996933 0.825816977489547 0.748151592823709 0.913337361501670
result(:,:,3) =
0.004634224134067 0.078175528753184 0.106652770180584 0.538342435260057
0.961898080855054 0.774910464711502 0.442678269775446 0.996134716626885

Related

How to repeat every 3rd element of a vector?

I have a vector like this:
h = [1,2,3,4,5,6,7,8,9,10,11,12]
And I want to repeat every third element like so:
h_rep = [1,2,3,3,4,5,6,6,7,8,9,9,10,11,12,12]
How do I accomplish this elegantly in MATLAB? The actual arrays are huge, so ideally I don't want to write a for loop. Is there a vectorized way to do this?
One way to do this would be to use the recent repelem function that was released in version R2015b where you can repeat each element in a vector a certain amount of times. In this case, specify a vector where every third element is a 2 with the rest of the values being a 1 as the number of times to repeat the corresponding element, then use the function:
N = numel(h);
rep = ones(1, N);
rep(3:3:end) = 2;
h_rep = repelem(h, rep);
Using your example: h = 1 : 12, we thus get:
>> h_rep
h_rep =
1 2 3 3 4 5 6 6 7 8 9 9 10 11 12 12
If repelem is not available to you, then a clever use of cumsum may help. Basically, note that for every three elements, the next one is a copy of the previous element. If we had an indicator vector of [1 1 1 0] where 1 is the position that we want to copy and 0 tells us to copy the last value, using cumulative sum or cumsum on repeated versions of this vector - exactly 1 + (numel(h) / 4) will give us exactly where we would need to index into h. Therefore, create a vector of ones that is the length of h added with 1 + (numel(h) / 4 to ensure that we make space for the duplicate elements, then make sure every fourth element is set to 0 before applying the cumsum:
N = numel(h);
rep = ones(1, N + 1 + (N / 4));
rep(4:4:end) = 0;
rep = cumsum(rep);
h_rep = h(rep);
Thus:
>> h_rep
h_rep =
1 2 3 3 4 5 6 6 7 8 9 9 10 11 12 12
One last suggestion (thanks to user #bremen_matt) would be to reshape your vector into a matrix so that it has 3 rows, duplicate the last row, then reshape the resulting duplicated matrix back to a single vector:
h_rep = reshape(h, 3, []);
h_rep = reshape([h_rep; h_rep(end,:)], 1, []);
We again get:
>> h_rep
h_rep =
1 2 3 3 4 5 6 6 7 8 9 9 10 11 12 12
Of course the obvious caveat with the above code is that the length of vector h is evenly divisible by 4.
(Modified according to rayryeng's correct observations)...
Another solution is to play around with the reshape function. If you reshape the matrix to a 3xn matrix first...
B = reshape(h,3,[])
And then copy the last row
B = [B;B(end,:)]
And finally vectorize the solution...
B(:).'
You can use just indexing:
h = [1,2,3,4,5,6,7,8,9,10,11,12]; % initial data
n = 3; % step for repetition
h_rep = h(ceil(n/(n+1):n/(n+1):end));
An index-based approach (using sort):
h_rep = h(sort([1:numel(h) 3:3:numel(h)]));
Or a slightly shorter syntax...
h_rep = h(sort([1:end 3:3:end]));
I think this will do it:
h = [1,2,3,4,5,6,7,8,9,10,11,12];
h0=kron(h,[1 1])
h_rep=h0(mod(1:length(h0),2)==0 | mod(1:length(h0),3)==2)
Answer:
1 2 3 3 4 5 6 6 7 8 9 9 10 11 12 12
Explanation:
After duplicating every element, you select only those that you wants. You can extend this idea to duplicate second and third. etc..

Chain elements in matrix using rows

I have a matrix where each row is a combination of two numbers, like A = [1 2; 2 5; 3 4; 4 6; 5 6]
A is built so that, for each row, the first elements is always smaller than the second one.
I need to return, from A, the lists of chained elements (in the case above, the lists of chained elements are 1 2 5 6 and 3 4 6). These lists are essentially built by considering a row, and checking is the last number is the first number of another row. Do you have any suggestion on how to do this?
If I got the question correctly, assuming A as the input array, you can use bsxfun -
mask = bsxfun(#eq,A(:,1),A(:,2).');
out = unique(A(any(mask,1).' | any(mask,2),:))
Sample run -
>> A
A =
1 2
3 4
2 5
5 6
>> mask = bsxfun(#eq,A(:,1),A(:,2).');
>> unique(A(any(mask,1).' | any(mask,2),:))
ans =
1
2
5
6
You can also use ismember, like so -
out = unique(A(ismember(A(:,1),A(:,2)) | ismember(A(:,2),A(:,1)),:))
Third option would be to use intersect to solve it, like so -
[~,idx1,idx2] = intersect(A(:,1),A(:,2));
out = unique(A([idx1,idx2],:))
The following seems to work. It builds a matrix (B) that tells which elements are connected (by 1 step). It then extends that matrix ( C) to include 0-step, 1-step, ..., (n-1)-step connections, where n is the number of nodes.
From that matrix, groups of connected elements are obtained (R). Finally, only "maximal" groups are kept (that is, those, not contained in other groups).
A = [1 2; 3 4; 2 5; 4 6; 5 6]; %// data
n = max(A(:));
B = full(sparse(A(:,1), A(:,2), 1, n, n )); %// matrix of 1-step connections
C = eye(n) | B; %// initiallize with 0-step and 1-step connections
for k = 1:n-1
C = C | C*B; %// add k-step connections, up to k=n-1
end
[ii, jj] = find(C);
R = accumarray(ii, jj, [], #(x) {sort(x).'}); %'// all groups (maximal or not)
[xx, yy] = ndgrid(1:n);
C = cellfun(#(x,y) all(ismember(x, y)), R(xx), R(yy) ); %// group included in another?
result = R(all(~C | eye(n), 2)); %// keep only groups that are not included in others
This gives
>> result{:}
ans =
1 2 5 6
ans =
3 4 6

average operation in the first 2 of 3 dimensions of a matrix

Suppose A is a 3-D matrix as below (2 rows-2 columns-2 pages).
A(:,:,1)=[1,2;3,4];
A(:,:,2)=[5,6;7,8];
I want to have a vector, say "a", whose inputs are the average of diagonal elements of matrices on each page. So in this simple case, a=[(1+4)/2;(5+8)/2].
But I have difficulties in matlab to do so. I tried the codes below but failed.
mean(A(1,1,:),A(2,2,:))
You can use "partially linear indexing" in the two dimensions that define the diagonal, as follows:
Since partially linear indexing can only be applied on trailing dimensions, you first need to apply permute to rearrange dimensions, so that the first and second dimensions become second and third.
Now you leave the first dimension untouched, linearly-index the diagonals in the second and third dimensions (which effectly reduces those two dimensions to one), and apply mean along the (combined) second dimension.
Code:
B = permute(A, [3 1 2]); %// step 1: permute
result = mean(B(:,1:size(A,1)+1:size(A,1)*size(A,2)), 2); %// step 2: index and mean
In your example,
A(:,:,1)=[1,2;3,4];
A(:,:,2)=[5,6;7,8];
this gives
result =
2.5000
6.5000
You can use bsxfun for a generic solution -
[m,n,r] = size(A)
mean(A(bsxfun(#plus,[1:n+1:n^2]',[0:r-1]*m*n)),1)
Sample run -
>> A
A(:,:,1) =
8 4 1
7 6 3
1 5 8
A(:,:,2) =
1 7 6
8 5 2
1 2 7
A(:,:,3) =
6 2 8
1 1 6
1 4 5
A(:,:,4) =
8 1 6
1 5 1
9 2 7
>> [m,n,r] = size(A);
>> sum(A(bsxfun(#plus,[1:n+1:n^2]',[0:r-1]*m*n)),1)
ans =
22 13 12 20
>> mean(A(bsxfun(#plus,[1:n+1:n^2]',[0:r-1]*m*n)),1)
ans =
7.3333 4.3333 4 6.6667

adding values to diagonals of matrix using element-wise addition in matlab

I am writing a script that operates on matrices, and I have run into the problem of needing to add the sum of the diagonals of a previous matrix to the diagonal elements of a new matrix. The code I have so far for this particular function (described in more detail below) is:
t = 1;
for k = (m-1):-1:-(m-1)
C = bsxfun(#plus, diag(B, k), d);
g(t) = sum(diag(B, k));
t = t + 1;
end
where d is a 1x3 array, and C is supposed to be a 3x3 array; however, C is being output as a 1x3 array in such a way that the first diagonal is being summed and added to d, then the main diagonal is being summed and added to d, and the final diagonal is being summed and added to d.
Is there a way I can get the values of C to be such that the first diagonal is the sum of it's individual elements added to the last element of d, the main diagonal's individual elements added to the middle element of d, and the bottom diagonal's elements added to the first element of d? (while still working for any array size?)
Here is a picture that describes what I'm trying to achieve:
Thanks!
You can use toeplitz to generate a matrix containing the values that need to be added to your original matrix:
M = [5 5 5; 7 7 7; 9 9 9]; %// data matrix
v = [1 11 4 3 2]; %// data vector
S = toeplitz(v);
S = S(1:(numel(v)+1)/2, (numel(v)+1)/2:end);
result = M+S;
Or, as noted by #thewaywewalk, you can do this more directly as follows:
M = [5 5 5; 7 7 7; 9 9 9]; %// data matrix
v = [1 11 4 3 2]; %// data vector
result = M + toeplitz(v(size(M,1):-1:1), v(size(M,2):end));
Assuming B to be a square shaped matrix, listed in this post would be one bsxfun based vectorized approach. Here's the implementation -
N = size(B,1) %// Store size of B for later usage
%// Find a 2D grid of all indices with kth column representing kth diagonal of B
idx = bsxfun(#plus,[N-numel(B)+1:N+1:N]',[0:2*N-2]*N) %//'
%// Mask of all valid indices as we would see many from the 2D grid
%// going out of bounds of 2D array, B
mask = idx>numel(B) | idx<1
%// Set all out-of-bounds indices to one, so that in next step
%// we could index into B in a vectorized manner and sum those up with d
idx(mask)=1
sum1 = bsxfun(#plus,B(idx),d(:).') %//'
%// Store the summations at proper places in B with masking again
B(idx(~mask)) = sum1(~mask)
Sample run -
B =
1 9 0
7 9 4
6 8 7
d =
4 9 5 8 2
B =
6 17 2
16 14 12
10 17 12
Code:
The following code adds the sums of the diagonals of A to the corresponding diagonals in the matrix B. The code works for matrices A, B of equal size, not necessarily square.
A = magic(4);
B = magic(4);
D = bsxfun(#minus, size(A,2)+(1:size(A,1)).', 1:size(A,2)); %'
sumsDiagsA = accumarray(D(:), A(:)); %// Compute sums of diagonals (your 'd')
B = B + sumsDiagsA(D); %// Add them to the matrix
Explanation:
First we build a matrix that numbers all diagonals beginning from the rightmost diagonal:
>> D = bsxfun(#minus, size(A,2)+(1:size(A,1)).', 1:size(A,2))
D =
4 3 2 1
5 4 3 2
6 5 4 3
7 6 5 4
Then we compute sumsDiagsA as the sum of the diagonals via accumarray:
sumsDiagsA = accumarray(D(:), A(:));
The variable sumsDiagsA is what you refer to as d in your code.
Now we use indexing to the vector containing the sums and add them to the matrix B:
C = B + sumsDiagsA(D);
Assuming you have already computed your vector d, you don't need the accumarray-step and all you need to do is:
D = bsxfun(#minus, size(B,2)+(1:size(B,1)).', 1:size(B,2)); %'
C = B + d(D);

matlab: eliminate elements from array

I have quite big array. To make things simple lets simplify it to:
A = [1 1 1 1 2 2 3 3 3 3 4 4 5 5 5 5 5 5 5 5];
So, there is a group of 1's (4 elements), 2's (2 elements), 3's (4 elements), 4's (2 elements) and 5's (8 elements). Now, I want to keep only columns, which belong to group of 3 or more elements. So it will be like:
B = [1 1 1 1 3 3 3 3 5 5 5 5 5 5 5 5];
I was doing it using for loop, scanning separately 1's, 2's, 3's and so on, but its extremely slow with big arrays...
Thanks for any suggestions how to do it in more efficient way :)
Art.
A general approach
If your vector is not necessarily sorted, then you need to run to count the number of occurrences of each element in the vector. You have histc just for that:
elem = unique(A);
counts = histc(A, elem);
B = A;
B(ismember(A, elem(counts < 3))) = []
The last line picks the elements that have less than 3 occurrences and deletes them.
An approach for a grouped vector
If your vector is "semi-sorted", that is if similar elements in the vector are grouped together (as in your example), you can speed things up a little by doing the following:
start_idx = find(diff([0, A]))
counts = diff([start_idx, numel(A) + 1]);
B = A;
B(ismember(A, A(start_idx(counts < 3)))) = []
Again, note that the vector need not to be entirely sorted, just that similar elements are adjacent to each other.
Here is my two-liner
counts = accumarray(A', 1);
B = A(ismember(A, find(counts>=3)));
accumarray is used to count the individual members of A. find extracts the ones that meet your '3 or more elements' criterion. Finally, ismember tells you where they are in A. Note that A needs not be sorted. Of course, accumarray only works for integer values in A.
What you are describing is called run-length encoding.
There is software for this in Matlab on the FileExchange. Or you can do it directly as follows:
len = diff([ 0 find(A(1:end-1) ~= A(2:end)) length(A) ]);
val = A(logical([ A(1:end-1) ~= A(2:end) 1 ]));
Once you have your run-length encoding you can remove elements based on the length. i.e.
idx = (len>=3)
len = len(idx);
val = val(idx);
And then decode to get the array you want:
i = cumsum(len);
j = zeros(1, i(end));
j(i(1:end-1)+1) = 1;
j(1) = 1;
B = val(cumsum(j));
Here's another way to do it using matlab built-ins.
% Set up
A=[1 1 1 1 2 2 3 3 3 3 4 4 5 5 5 5 5];
threshold=2;
% Get the unique elements of the array
uniqueElements=unique(A);
% Count haw many times each unique element occurs
counts=histc(A,uniqueElements);
% Write which elements should be kept
toKeep=uniqueElements(counts>threshold);
% Make a logical index
indexer=false(size(A));
for i=1:length(toKeep)
% For every unique element we want to keep select the indices in A that
% are equal
indexer=indexer|(toKeep(i)==A);
end
% Apply index
B=A(indexer);

Resources