---
jupyter:
  jupytext:
    notebook_metadata_filter: all,-language_info
    split_at_heading: true
    text_representation:
      extension: .Rmd
      format_name: rmarkdown
      format_version: '1.2'
      jupytext_version: 1.13.7
  kernelspec:
    display_name: Python 3
    language: python
    name: python3
---

# Logical operations on Boolean arrays

`np.logical_and`, `np.logical_or`:

Sometimes we want to combine Boolean values using logical operators like AND,
OR, NOT.  This is straightforward for Python Booleans:

```{python}
# Logical AND - True only if both are True
print(True and True)
print(True and False)
```

```{python}
# Logical OR - True if either or both are True
print(True or True)
print(True or False)
print(False or False)
```

```{python}
# Logical NOT - inverts truth value
print(not True)
print(not False)
```

We have to do a little more work for *arrays* of Booleans, because the Python
`and`, `or`, `not` operators only return a single Boolean values, and so
do not operate as we expect on arrays:

```{python}
import numpy as np
```

```{python}
bool1 = np.array([True, True, False, False])
bool2 = np.array([False, True, False, True])
```

```{python tags=c("raises-exception")}
bool1 and bool2
```

```{python tags=c("raises-exception")}
bool1 or bool2
```

To do elementwise AND, OR, NOT, we can use `np.logical_and, np.logical_or,
np.logical_not`:

```{python}
# "logical_and" True where both of bool1 and bool2 are True
np.logical_and(bool1, bool2)
```

```{python}
# "logical_or" True where either of bool1 and bool2 are True
np.logical_or(bool1, bool2)
```

```{python}
# "logical_not" True where input array is False
np.logical_not(bool1)
```

## Using the bitwise operators

Equivalently, the `&`, `|` and `~` operators are applied elementwise.

These are called *bitwise* operators, for reasons we do not need to go into here.  *Iff applied to Boolean values* then:

* `&` gives the same result as `np.logical_and`
* `|` gives the same result as `np.logical_or`
* `~` gives the same result as `np.logical_not`

```{python}
bool1 & bool2
```

```{python}
bool1 | bool2
```

```{python}
~bool1
```

## Bitwise, brackets

**Be careful when using the bitwise operators**.  The bitwise operators have
relatively high operator precedence, meaning that Python will prefer to apply
the bitwise operator *before* other operators, such as comparison operators.


For example, consider these arrays, and the Boolean arrays from their comparison:

```{python}
first = np.array([1, 0, 1])
first == 1
```

```{python}
second = np.array([0, 1, 1])
second == 1
```

```{python tags=c("raises-exception")}
# This will give an error.  Why?
first == 1 & second == 1
```

The problem is that Numpy registers `&` as having [higher operator
preference](https://docs.python.org/3/reference/expressions.html#operator-precedence)
than `==`, so it does the `&` operation before the `==`, meaning that the code
above is equivalent to:

```{python tags=c("raises-exception")}
first == (1 & second) == 1
```

Therefore, you get the error like this:

```{python}
# Python is doing this under the hood in the statement above.
res = 1 & second
```

```{python tags=c("raises-exception")}
# Python next does this, generating the error.
first == res == second
```

The exact reason this last statement gives an error is [a little
advanced](https://docs.python.org/3/reference/expressions.html#comparisons). It
is because Python *internally* translates the statement above to:

```{python tags=c("raises-exception")}
# Python internally translates "first == res == second) to:
(first == res) and (res == second)
```

The problem with this internal translation is that `and` does not work when
comparing arrays:

```{python tags=c("raises-exception")}
# "and" does not work on arrays.
np.array([True, False]) and np.array([True, False])
```

The fix is to specify that you want the `==` operation done *before* the `&`
operation, using parentheses:

```{python}
# Guarantee the order of operations with parentheses.
(first == 1) & (second == 1)
```

To avoid worrying about this problem, you might prefer to use `np.logical_and`
etc:

```{python}
# The same operation using logical_and
np.logical_and(first == 1, second == 1)
```
