MATLAB find on multiple columns to multiple column result

Viewed 48

Setup

I have an array of captured data. The data may be captured on just 1 device or up to a dozen devices, with each device being a column in the array. I have a prior statement which I execute on the array to then turn it into a logical array to find particular points of interest in the data. Due to the nature of the data, there are many 0's and only a few 1's. I need to return an array with the indices of the 1's so I can go back and capture the data between those points (see update below).

find is an obvious choice for a function - however, the result I need, needs to have 1 column for each device. Normally find will do a linear index regardless of the dimensions of the array.

The devices follow a pattern - but aren't exactly the same. So, complicating this is the fact that the number of 1's in each column is close to, but not guaranteed to be exactly the same depending on the exact timing the data capture is stopped (they are most often different from each other by 1 element, but could be different by more).

MATLAB CODE ATTEMPTS

Because of that difference, I can't use the following simple code:

for p = 1:np
    indices( :, p ) = find( device.data.cross( :, p ) );
end

Notes:

  • np is the number of columns in the data = number of devices captured.
  • devices is a class representing the collection of devices
  • data is a TimeTable containing captured data on all the devices
  • cross is a column in the data TimeTable which contains the logical array
  • Even this simple code is inefficient and generates the Code Analyzer warning:

The variable 'indices' appears to change size on every loop iteration (within a script). Consider preallocating for speed.

As expected, it doesn't work as I get an error similar to the following:

Unable to perform assignment because the size of the left side is 448-by-1 and the size of the right side is 449-by-1.

I know why I get this error - each column in an array in MATLAB has to have the same number of rows, so I can't make the assignment if the row size doesn't match. I need to pad the "short" columns somehow. In this case, repeating the last index will work for my later operations without causing an error.

I can't figure out a "good" way to do this. I can't pre-populate the array rows because I don't know how many rows there will be until I've done the find operation.

I can change the code as follows:

    indices = [];
    for p = 1:np
        tempindices = find( devices.data.cross(:, p) );
        sizediff = size( tempindices, 1 ) - size( indices, 1 );
        
        if p > 1
            
            if sizediff > 0

                padding = repmat(indices(end, 1:(p - 1)), sizediff, 1);
                indices = [indices; padding];

            elseif sizediff < 0

                padding = repmat(tempindices(end), abs(sizediff), 1);
                tempindices = [tempindices; padding];

            end
            
        end

        indices(:,p) = tempindices;
        
    end

Note: padarray would have been useful here, but I don't have the Image Processing Toolbox so I cannot use it.

This code works, but it is very inefficient, it creates multiple otherwise unneeded variables in the workspace and generates multiple "appears to change size on every loop iteration" warnings in Code Analyzer. Is there a more efficient way to do this?

Update / Additional Information:

Some more context is needed for my issue. Given that devices.data.cross is a logical array, to just "pick" the data I want from other columns in my table (as I originally described my problem) I could select a column from devices.data.cross and pass that logical column as a subscript to get that data. I do that where it works. However, for some of the columns I need to select "chunks" of the data between the indices and that's where (I think) I need the indices. Or, at least I don't know of another way to do it.

Here is example code of how I use the indices:

for p = 1:np
    for i = 2:num_indices

        these_indices = indices(i-1, p):( indices(i, p) - 1 );
        rmsvoltage = sqrt( mean( devices.data.voltage(these_indices).^2 ) );

    end

end

This is just one routine I do on the "chunks" of data. I also have a couple of functions where these chunks of data are passed for processing.

1 Answers

When I understood your problem correctly, the code below should work. I'm using the approach that Cris Luengo suggested in a comment under your question.

Key element is [rowIdcs, colIdcs] = find( cros ); which gives you the subscripts of positions in cros having a value of one. Please find further comments inline.

% Create some data for testing
volt = randn(10,10);
cros = randi(10,10,10) > 9;

% Get rowIdcs and colIdcs, which have both a size of Nx1, 
% with N denoting the number of ones in the mask.
% rowIdcs and colIdcs are the subscripts of the ones in the mask.
[rowIdcs, colIdcs] = find( cros );

% The number of chunks is equal to number N of ones found in the mask;
numChunks = numel( rowIdcs ); 

% Initilize a vector for the rms
rms = zeros( numChunks, 1 );

% Loop over the chunks
for k = 1 : numChunks
   
    curRow = rowIdcs(k);
    curCol = colIdcs(k);
    
    % Get indices of range over neighbouring rows
    chunkRowIdcs = curRow + [-1 0 1]; %i.e. these_indices in your example
    
    % Remove indices that are out of range
    chunkRowIdcs( chunkRowIdcs < 1 | chunkRowIdcs > size(volt,1) ) = [];
    
    % Get voltages covered by chunk 
    chunkVoltages = volt( chunkRowIdcs, curCol );
    
    % Get RMS over voltages
    rms(k) = sqrt( mean( chunkVoltages(:).^2 ));
    
end
Related