rpb
rpb

Reputation: 3299

Efficient ways of converting in between numpy boolen into two states with Python

The objective is to create a boolean array, state, from another boolean array, initial, according to the rules described below.

If initial starts with False, the first and subsequent elements of state will be False. Upon reaching True in initial, state will switch to True.

Example:

initial: [False, False, False, True]
state: [False, False, False, True]

If, on the other hand, initial starts with True, the first and subsequent elements of state will also be True. Upon reaching True in initial, state will switch to False.

Example:

initial: [True, False, False, True]
state: [True, True, True, False]

If True is encountered in the middle of initial, the new value of state will depend on the previous state. For example, if the previous value of state was False, then state will switch to True:

initial: [False, False, False, True]  --Take note of the last boolean
state: [False, False, False, True]  --Take note of the last state i.e., False

On the other hand, if the previous value of state was True, then state will switch to False:

initial: [False, False, False, True]  --Take note of the last boolean
state: [True, True, True, False]  --Take note of the last state i.e., False

Based on these requirements and two simple samples of initial, I've written the following code:

import numpy as np

sample_1 = [False, False, False, False, False, True, False, False, False, True, False, False, False]
# sample_2  = [True, False, False, False, False, True, False, False, False, True, False, False, False]

expected_output = []
counter = 0
for x in np.array(sample_1):
    if x == False and counter == 0:
        idx = False
    elif x == True and counter == 0:
        idx = True
        counter = 2
    elif x == False and counter == 1:
        idx = True
    elif x == True and counter == 1:
        idx = True
        counter = 0
    elif x == True and counter == 2:
        idx = False
        counter = 0
    expected_output.append(idx)

The expected output, respectively for sample1 and sample2 is:

For sample_1:

[False, False, False, False, False, True, True, True, True, False, False, False, False]

For sample_2:

[True, True, True, True, True, True, False, False, False, True, True, True, True]

I am curios whether there is more compact notation or a build-in module that can be used instead of the naive if-else approach above.

Upvotes: 1

Views: 74

Answers (2)

GZ0
GZ0

Reputation: 4268

IIUC, this gives what you want

import numpy as np

def compute_states(initial: np.array):
    return (initial.cumsum() % 2).astype(bool)

Test:

print(compute_states(np.array([False, False, False, False, False, True, False, False, False, True, False, False, False])))
print(compute_states(np.array([True, False, False, False, False, True, False, False, False, True, False, False, False])))

Output:

[False False False False False  True  True  True  True False False False False]
[ True  True  True  True  True False False False False  True  True  True  True]

Upvotes: 2

Mad Physicist
Mad Physicist

Reputation: 114468

To begin with, your if/else can be greatly simplified. Notice that any time you get a True element, the value of future elements in the output is toggled.

You can therefore safely always start your loop with the assumption that prior elements were False:

state = False
for x in sample_1:
    if x:
        state = not state
    expected_output.append(state)

A similar approach with a counter:

counter = 0
for x in sample_1:
    if x:
        counter += 1
    expected_output.append(bool(counter % 2))

You can if course do this with numpy functions. A term that I've heard for this type of operation is "smearing a mask". The general idea is that you can build a mask with runs of elements by taking the cumulative sum of an array containing 1, 0 and -1 elements. 1 turns on a run, and -1 turns it off.

To convert your input data into a more useful format, use np.flatnonzero:

indices = np.flatnonzero(sample_1)

Now make an intermediate array that will contain your +/-1 values. I'm going to suggest using np.int8 for that. First, it takes up less space, and second, you can do the entire operation in-place if you're clever:

triggers = np.zeros(len(sample_1), dtype=np.int8)

Place ones at every other index location:

triggers[indices[::2]] = 1

Place negative ones at every other location:

triggers[indices[1::2]] = -1

The output mask is just the cumulative sum of the triggers:

expected_output = np.cumsum(triggers).astype(bool)

You can do the same operation without allocating a single temporary array in the last step, because np.bool_ and np.int8 have the same element size:

expected_output = np.cumsum(triggers, out=triggers).view(np.bool_)

Upvotes: 2

Related Questions