---
author:
- "by [Eugeniy E. Mikhailov](http://physics.wm.edu/~evmik/) and [Greg
  Bentsen](https://physics.wm.edu/~gbentsen/)"
title: Boolean algebra, conditional statements, loops.
---

# Boolean algebra

## Boolean algebra

Variable of boolean type can have only two values

::: incremental
-   false (Python uses `False`{.python} (case is important) and
    recognizes `0`{.python} as a substitute)
-   true (Python uses `True`{.python}, also `1`{.python} to indicate
    true, but actually everything except zero can be used!)
:::

. . .

There are three logical operators which are used in boolean algebra

::: incremental
-   $\lnot$ - logic **not**, Python `not`{.python} $$\begin{aligned}
                    \lnot \text{true = false}\\
                    \lnot \text{false = true}
    \end{aligned}$$
-   $\land$ - logic **and**, Python `and`{.python} $$\begin{aligned}
                    A \land B =
                    \begin{cases}
                        \text{true}, \ \text{if A=true~and~B=true}, \\
                        \text{false}, \ \text{otherwise}
                    \end{cases}
    \end{aligned}$$
-   $\lor$ - logic **or**, Python `or`{.python} $$\begin{aligned}
                    A \vee B =
                    \begin{cases}
                        \text{false}, \ \text{if A=false~and~B=false}, \\
                        \text{true}, \ \text{otherwise}
                    \end{cases}
    \end{aligned}$$
:::

## Boolean operators precedence in Python

Assume

``` python
A=False
B=True
C=True
```

what is the resulting value of

``` python
A or not B and C
```

. . .

`not`{.python} has highest precedence, then `and`{.python}, and then
`or`{.python}

. . .

``` python
A or ( (not B) and C )
```

. . .

Thus

``` python
>>> A or not B and C
False
```

. . .

"Cat is an animal and cat is not an animal"

. . .

is a false statement

. . .

``` python
>>> C and not C
False
```

## Fun with boolean logic

::: columns
::: {.column width="45%"}
"To be or not to be"\
The answer is always `\alert{True}`{=tex}
:::

::: {.column width="45%"}
``` python
>>> B=True
>>> B or not B
True
>>>
>>> B=False
>>> B or not B
True
```
:::
:::

. . .

There is an island, which is populated by two kind of people: liars and
truthlovers.

-   Liars always lie and never speak a word of truth.
-   Truthlovers always speak only truth.

Suppose you land on this island and meet a person. What will be the
answer to your question "Who are you?"

. . .

-   The answer always will be "Truthlover".

. . .

Now you see a person who answers to your question. "I am a liar."

Is it possible?

. . .

-   This makes a paradox and should not ever happen on this island.

## Python boolean edge cases

``` python
>>> True and 13
13
>>> True or 13
True
>>> 123.3 and 12
123.3
>>> 123.3 or 14
123.3
>>> True + True
2
>>> False + True
1
```

`\alert{Do not make you program depending on strange mix of logic and numbers math.}`{=tex}

It is very hard to wrap your mind around such use.

# Comparison operators

## Comparison operators

::: center
   Math or human notation                        Python
  ------------------------ --------------------------------------------------
            $=$             `==`{.python} `\alert{double equal sign!}`{=tex}
           $\ne$                             `!=`{.python}
            $<$                               `<`{.python}
           $\le$                             `<=`{.python}
            $>$                               `>`{.python}
           $\ge$                             `>=`{.python}
:::

::: columns
::: {.column width="30%"}
``` python
>>> 2 > 3
False
>>> 2 == 2
True
>>> 23 != 3
True
```
:::

::: {.column width="60%"}
``` python
>>> x=[1,2,3,4,5]
>>> x>3
Traceback (most recent call last):
  File "<python-input-148>", line 1, in <module>
    x>3
TypeError: '>' not supported between instances of 'list' and 'int'
```

But note that the comparison would `\alert{fail with lists}`{=tex}

We need to use the array data structure, e.g. `numpy`{.python} module's
`array`{.python}

``` python
>>> import numpy as np
>>> x=np.array([1,2,3,4,5])
>>> x
array([1, 2, 3, 4, 5])
>>> x > 3
array([False, False, False,  True,  True])
>>> x[x > 3]  # chose from x element larger than 3
array([4, 5])
```
:::
:::

## Comparison with arrays (specifically `numpy`{.python} arrays)

It is done element-wise, i.e. we apply comparison to every element
independently

::: columns
::: {.column width="45%"}
``` python
>>> A = np.array( [[1, 2], [3, 4]] )
>>> A
array([[1, 2],
       [3, 4]])
```
:::

::: {.column width="45%"}
``` python
>>> B = np.array( [[11, 22], [63, 64]] )
>>> B
array([[11, 22],
       [63, 64]])
```
:::
:::

``` python
>>> A >= 2
array([[False,  True],
       [ True,  True]])
```

We get back a 2D array

. . .

### Useful idiom: chose from ... where ... is true

::: columns
::: {.column width="30%"}
``` python
>>> A[A >= 2]
array([2, 3, 4])
```

We get back a `\alert{1D}`{=tex} array
:::

::: {.column width="30%"}
``` python
>>> B[A >= 2]
array([22, 63, 64])
```

We get back a `\alert{1D}`{=tex} array
:::
:::

# Conditional statements

## if-else statement

::: columns
::: {.column width="30%"}
``` python
if *expression* :
    # this part is executed only
    # if *expression* is true
else:
    #this part is executed only
    #if *expression* is false

# this part is not part of if
```
:::

::: {.column width="30%"}
``` python
if *hungry*:
    buy some food
else:
    keep working for 10 min
Check the clock
```
:::

::: {.column width="40%"}
``` python
if (x>=0):
    y=np.sqrt(x)
else:
    ValueError("x must be larger than 0")
```
:::
:::

Unconventional but technically correct

``` python
>>> if 5:
...     print(56)  # the only branch that will be taken
... else:
...     print(11)
...
56
>>> # we will alway see 56 above
```

## Common mistake in the 'if' statement

``` python
>>> x=3
>>> y=4
>>> if (x=y):
...     D=4
... else:
...     D=2
```

. . .

The value of 'D' is always 4, except the case when y=0

. . .

`\alert{assignment operator (=) is not equality operator (==)}`{=tex}

. . .

Modern python will warn you about this potential error.

`python`{.python} says

``` python
  File "<python-input-177>", line 1
    if (x=y):
        ^^^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
```

`ipython`{.python} says

``` python
  Cell In[1], line 3
    if (x=y):
        ^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
```

## Short form of 'if' without 'else' statement

We can skip `else`{.python} when we do not need it

::: columns
::: {.column width="30%"}
``` python
if *expression* :
    # this part is executed only
    # if *expression* is true
```
:::

::: {.column width="30%"}
``` python
if *won a million*:
    go party
keep doing as before
```
:::

::: {.column width="40%"}
``` python
if (delta<0.1):
    return x1
delta = x1 - x1
```
:::
:::

# Loops

## The 'while' loop

::: columns
::: {.column width="30%"}
``` python
while *expression*:
    this part is executed while *expression* is true
```
:::

::: {.column width="30%"}
``` python
while *hungry*:
    keep eating
```
:::

::: {.column width="40%"}
``` python
>>> x=1
>>> while (x<4):
...     print(x)
...     x = x + 1
...
1
2
3
```
:::
:::

`while`{.python} loop is extremely useful but they are not guaranteed to
finish.

For a bit more complicated conditional statement and the loop body, it
is hard to predict if the loop will finish.

. . .

Yet another common mistake is

``` python
>>> c=1
>>> i=1
>>> while (i<5):
...     print(c)
...     c=c+5
```

``\alert{We will never finish, since `i` is not changing within the loop.}``{=tex}

## The 'for' loop

::: columns
::: {.column width="60%"}
``` python
for variable in list_like:
    do something
```

In this case variable is assigned consequently with values taken from
`list_like`{.python} and then statements inside of the loop are executed
:::

::: {.column width="40%"}
``` {.python .numberLines}
>>> sum=0
... x=[1, 2, 7, 5]
... for v in x:
...     sum = sum + v
... print(sum)
15
```
:::
:::

. . .

`for`{.python} loops are guaranteed to complete after predictable number
of iterations (the amount of columns in *expression*).

# Series

## Example: sum a series of numbers

$$S=\sum_{i=0}^{100} i = 0 +  1 + 2 + 3 + 4 + \cdots + 99 + 100$$

::: columns
::: {.column width="45%"}
``` {.python .numberLines}
>>> S=0
>>> i=0;
>>> while(i<=100):
...     S=S+i
...     i=i+1
... print(f"{S=}")  # handy way to print
... print(f"{i=}")  # variable and its value
...
S=5050
i=101
```
:::

::: {.column width="45%"}
``` {.python .numberLines}
>>> S=0
... i=21321  # note: it will be reassigned
... for i in range(101):  # note 101 and not 100
...     S=S+i
... print(f"{S=}")
... print(f"{i=}")
...
S=5050
i=100
```
:::
:::

## Example

Calculate sum $$S=\sum_{k=5}^100 a_k$$ As long as $a_k \ge 10^{-5}$,
where $a_{k}=k^{-k}$.

::: columns
::: {.column width="50%"}
Using `while`{.python} loop

``` {.python .numberLines}
>>> S=0
... k=5
... while (k<=100):
...     a_k  = k**(-k)
...     k = k+1  # update loop controlling variable ASAP
...     if a_k < 10**(-5):
...         break  # new operator! break out of the loop
...     S = S + a_k
... print(S)
...
0.0003414334705075446
```
:::

::: {.column width="45%"}
Using `for`{.python} loop

``` {.python .numberLines}
>>> S=0
... for k in range(5,101):  # note +1 to the upper end
...     a_k  = k**(-k)
...     if a_k < 10**(-5):
...         break  # new operator! break out of the loop
...     S = S + a_k
... print(S)
...
0.0003414334705075446
```

Arguably, `for`{.python} loop is less work
:::
:::

## Same example with array operations

Calculate sum $$S=\sum_{k=5}^100 a_k$$ As long as $a_k \ge 10^{-5}$,
where $a_{k}=k^{-k}$.

When dealing with numerical operations, often it is more elegant to use
Python array operators. It is also much faster if the body of the loop
is executed many times (\> 1000).

``` {.python .numberLines}
>>> import numpy as np
... k = np.arange(5,101)  # again notice +1 to the upper end
... k = 1.0*k  # this looks useless, but it converts integer to float needed for `np.pow`
... a_k = np.pow(k, -k)
... S  = np.sum( a_k[a_k >= 10**(-5)] )
... print(S)
...
0.0003414334705075446
```

Note

-   use of the *choose elements* construct
-   built in `np.sum`{.python} function
-   `\alert{inconsistency}`{=tex} between base python power operation
    and `numpy` module
