# Notebook from wooshrow/course-python
Path: Lecturenotes/11 Regular Expressions.ipynb

# Module 11: Regular Expressions
March 19, 2021_____no_output_____Last time looked at the CRISP-DM reference model for approaching data analysis problems, and dicussed some key functionalities of popular and important data science libraries.

Today we will look at something with a quite [geeky reputation](https://xkcd.com/208/): regular expressions. They can be very useful for finding patterns in text (and that not only in Python programs!), and therefore software developers should know about them.

Next time we will dive deeper into the concepts of object orientation and object-oriented programming in Python._____no_output_____## String Manipulation

Regular expressions allow us to do advanced things with strings. Let's first recap Python's basic string manipulation features. We have already seen, for example, that the "+" operator is also defined for strings, that `isAlpha()` can be used to check if something is an alphabetical character, that the `lower()` method can be applied to strings to turn all contained characters into lower case (if they are not already), and some other string manipulation functionality. In fact, the string class contains several such methods. For example:

 ```str.capitalize()``` capitalizes the first letter and lowercases the rest

 ```str.find()``` returns the first occurrence of a particular substring in the string

 ```str.join()``` concatenates the strings that are returned by an iterable

 ```str.replace()``` replaces a particular substring by another string

 ```str.split()``` splits a string at a given separation character

 ```str.splitlines()``` splits a multiline string into lines

 ```str.strip()``` strips leading and trailing whitespaces from a string

 ```str.upper()``` uppercases all characters in the string

See https://docs.python.org/3/library/stdtypes.html#string-methods for full reference.

The functions above are all defined for normal, plain strings. Especially for functions like find and replace, however, it is in practice however often useful to *"look for something formatted like ..."* instead. For example, imagine you want to find some unknown e-mail address in a long text, or you want to add the country code to everything that looks like a Dutch mobile phone number. This is where regular expressions can help._____no_output_____## Regular Expressions

Regular expressions are strings that describe patterns. They are not only useful for programmers, but also appear in common user software. Wildcards (often * and ?), filter criteria for text search (pattern matching) and the find/replace feature in text editors (many support regular expressions) are some examples you might have come across earlier. With the ```re``` module Python supports regular expression operations (https://docs.python.org/3/library/re.html).

For instance, the function ```re.findall(pattern,string)``` can be used to find all occurrences of a pattern in a string and returns them as a list of strings. We will use this function for the examples in the following and discuss some other functions later on._____no_output_____### Literals
The most basic element of regular expressions are the so-called **literals**, that is, individual characters or character sequences that must match literally. For example:_____no_output_____

import re
text = "How much wood would a woodchuck chuck if a woodchuck could chuck wood?"

print(re.findall("w", text))
print(re.findall("wood", text))['w', 'w', 'w', 'w', 'w', 'w']
['wood', 'wood', 'wood', 'wood']

### Meta-Characters and Escaping

Regular expressions are however not only about describing strings literally, but even more so about sets of strings with particular properties. Therefore, characters are needed that are not interpreted literally. **Meta-characters** are such literals that have a special, sometimes context-dependent meaning:

\[ \] ( )  { } |  ?  +  -  *  ^  $  \ .

We will look at their meaning shortly. For now just note that their interpretation as meta-character can be switched off by putting the backslash character '\\' in front. This is also called "**escaping**". For example, the regular expression for describing "1+1=2" is "1\\+1=2", the regular expression for describing "C:\temp" is "C:\\\temp", and the regular expression for describing "wood?" is "wood\\?":_____no_output_____

print(re.findall("wood?", text))
print(re.findall("wood\?", text))['wood', 'wood', 'wood', 'wood']
['wood?']

Note that the **standard escapes** (such as the newline "\n" and the horizontal tab "\t") can also be used in regular expressions._____no_output_____### Character Classes
Pairs of box brackets ("\[" and "\]") are used to indicate **character classes**, that is, sets of characters from which exactly one is chosen when matching the regular expression to a text. For example, the regular expression "gr\[ae\]y" matches both "gray" and "grey", and "\[Ww\]ood" matches the word "wood" in upper and lower case alike:_____no_output_____

print(re.findall("[Ww]ood", text))['wood', 'wood', 'wood', 'wood']

Negation ("^") can be used to match any character except for those listed. For example, "wo\[^o\]" matches all character triples that start with "wo" and do not have an "o" at the third position:_____no_output_____

print(re.findall("wo[^o]", text))['wou']

Character classes can also use ranges of characters (corresponding to their order in the ASCII table). For example, "\[0-9A-F\]" will match hexadecimal digits, "\[5-9\]\[0-9\]" all numbers between 50 and 99, and "\[a-mA-M\]\[n-zN-Z\]" all pairs of characters where the first is from the first half and the second from the second half of the alphabet:_____no_output_____

print(re.findall("[a-mA-M][n-zN-Z]", text))['Ho', 'mu', 'hu', 'hu', 'hu', 'co', 'hu']

Apart from the "^" at the first position in a character class and the "-" used for defining ranges, special characters lose their special meaning within character classes. That is, the expression "\[-(+\*)^\]" will match any of the literal characters "-",  "(", "+", "\*", ")" and "^". A closing square bracket "]" can be included in a character class by escaping it ("\[\\]]").

To simplify things in practice a bit, Python includes a number of **predefined character** classes:

    .  matches any character (except or including the newline, depending on mode)

    \d corresponds to [0-9] and matches any digit

    \D corresponds to [^0-9] or [^\d] and matches any non-digit

    \s matches any whitespace character (including \t, \n, etc.)

    \S corresponds to [^\s] and matches any non-whitespace character

    \w matches any word characters (that is, characters that can be part of a word in any      language, numbers and the underscore)

    \W corresponds to [^\w] and matches all non-word characters  _____no_output_____### Boundary Matchers
In addition, there are a couple **boundary matchers** that can be used in regular expressions:

     ^  start of a string, or of a line (in multiline mode)

     $  end of a string, or of a line (in multiline mode)

     \b word boundary

     \B non-word boundary

     \A beginning of the input string

     \Z the end of the input string

For example, the regular expression "d\W" can be used to find all occurrences of "d" at the end of a word:_____no_output_____

print(re.findall("d\W", text))['d ', 'd ', 'd ', 'd?']

### Repetition Qualifiers

To be able to deal with patterns whose exact length is not known in advance, regular expressions comprise **repetition qualifiers**. For example, if the "?" is appended to a character, character class or more complex expression, it denotes that it is optional and will match zero or one occurrences of it:_____no_output_____

print(re.findall("wood\??", text))['wood', 'wood', 'wood', 'wood?']

Other repetition qualifiers are:

 	+     one or more repetitions
 	*     zero or more repetitions
 	{n}   exactly n repetitions
 	{n,m} min n and max m repetitions
 	{n,}  at least n repetitions

For example, we can use it to find all 3-5 character words in the text:_____no_output_____

print(re.findall("\w{3,5}\W", text))['How ', 'much ', 'wood ', 'would ', 'chuck ', 'chuck ', 'chuck ', 'could ', 'chuck ', 'wood?']

By default the repetitions quantifiers are "greedy", that is, they match as much of the input text as possible. To make repetition quantification non-greedy ("reluctant"), a "?" can simply be appended, causing the regular expression engine to try to match the expression using as few input text as possible. For example, the expression "A.\*a" matches "Anna-Lena" completely, while "A.\*?a" matches only "Anna". The following example illustrates the difference on our input text:_____no_output_____

print(re.findall("\w+", text))
print(re.findall("\w+?", text))['How', 'much', 'wood', 'would', 'a', 'woodchuck', 'chuck', 'if', 'a', 'woodchuck', 'could', 'chuck', 'wood']
['H', 'o', 'w', 'm', 'u', 'c', 'h', 'w', 'o', 'o', 'd', 'w', 'o', 'u', 'l', 'd', 'a', 'w', 'o', 'o', 'd', 'c', 'h', 'u', 'c', 'k', 'c', 'h', 'u', 'c', 'k', 'i', 'f', 'a', 'w', 'o', 'o', 'd', 'c', 'h', 'u', 'c', 'k', 'c', 'o', 'u', 'l', 'd', 'c', 'h', 'u', 'c', 'k', 'w', 'o', 'o', 'd']

### Capturing Groups
Round brackets around an expression denote a **capturing group**. That is, the result of matching the regular expression will be what is inside the group. A quantifier following a closing round bracket applies to the whole group. For example, "Uni(versiteit)? Utrecht" matches "Uni Utrecht" and "Universiteit Utrecht", but the returned result of the matching will be "versiteit" and "", respectively. In our running example, we can use it, for example, to find all words preceding an occurrence of "wood":_____no_output_____

print(re.findall("(\w+)\Wwood", text))['much', 'a', 'a', 'chuck']

This shows that capturing groups are useful to find specific, varying text that comes together with specific, fixed pieces of text. Another, maybe more practical example in this regard is to find the text between particular tags in a HTML or XML file:_____no_output_____

print(re.findall("(\w+)", \
                 "Bold font marks important words."))['important']

Capturing groups can also be nested, then returning results for the different capturing levels:_____no_output_____

print(re.findall("((\w+)\Wwood)", text))[('much wood', 'much'), ('a wood', 'a'), ('a wood', 'a'), ('chuck wood', 'chuck')]

Groups can be denoted as non-capturing by using "(?:" instead of a single round bracket at the beginning. This can be handy to structure more complex regular expressions, but without including all thus defined groups into the returned result. For example:_____no_output_____

print(re.findall("((?:\w+)\W(?:wood))", text))['much wood', 'a wood', 'a wood', 'chuck wood']

Another kind of non-capturing groups can be used to configure the behavior of a regular expression more precisely. After opening a group with "(?", one or more flags from the set 'a' (ASCII-only matching), 'i' (ignore case), 'L' (locale-dependent), 'm' (multi-line), 's' (dot matches all), 'u' (Unicode matching), 'x' (verbose) can be set. For example, the ignore-case flag can be used to match the file ending "pdf" to "pdf", "Pdf" and "PDF" alike:_____no_output_____

print(re.findall("(?i)pdf", "PDF, pdf, or Pdf?"))['PDF', 'pdf', 'Pdf']

The flags 'i', 's', 'm' and 'x' can also be switched off again with a preceding '-'._____no_output_____### Alternatives
We have seen earlier that character classes can be used to describe alternatives at the character level. With the symbol "|" it is also possible to denote longer more complex expressions as **alternatives**. They are tried from left to right in a non-greedy fashion, that is, once one alternative matches, it is accepted. For example:_____no_output_____

print(re.findall("wood|chuck|woodchuck", text))['wood', 'wood', 'chuck', 'chuck', 'wood', 'chuck', 'chuck', 'wood']

Another example (words ending on "od" or "uld"):_____no_output_____

print(re.findall("(\w+(ood|ould))", text))[('wood', 'ood'), ('would', 'ould'), ('wood', 'ood'), ('wood', 'ood'), ('could', 'ould'), ('wood', 'ood')]

Or with a non-capturing inner group:_____no_output_____

print(re.findall("(\w+(?:ood|ould))", text))['wood', 'would', 'wood', 'wood', 'could', 'wood']

### A Realisitic Example
Finally, let's have a look a realistic example. Suppose you have downloaded a plain-text sequence file from the EMBL database, say https://www.uniprot.org/uniprot/P05787.txt. We want to retrieve the information from the "RL" lines, that is, the common citation information for the reference, that is usually sufficient to find the papers in question. This can be done in Python as follows:
_____no_output_____

import re

with open("data/P05787.txt", "r") as file:
    dbentry = file.read()
reference_locations = re.findall("(?m)^RL\W+(.+)\.$", dbentry)

print(reference_locations)['Gene 86:241-249(1990)', 'Mol. Endocrinol. 4:370-374(1990)', 'New Biol. 2:464-478(1990)', 'J. Biol. Chem. 272:7556-7564(1997)', 'Nat. Genet. 36:40-45(2004)', 'Submitted (APR-2005) to the EMBL/GenBank/DDBJ databases', 'Nature 440:346-351(2006)', 'Submitted (JUL-2005) to the EMBL/GenBank/DDBJ databases', 'Genome Res. 14:2121-2127(2004)', 'Mol. Cell. Biol. 9:1553-1565(1989)', 'Differentiation 33:69-85(1986)', 'Electrophoresis 18:605-613(1997)', 'J. Biol. Chem. 267:3901-3906(1992)', 'J. Cell Biol. 117:583-593(1992)', 'Arch. Oral Biol. 45:879-887(2000)', 'J. Biol. Chem. 275:14910-14915(2000)', 'J. Biol. Chem. 277:10767-10774(2002)', 'J. Biol. Chem. 277:10775-10782(2002)', 'J. Cell Sci. 118:1081-1090(2005)', 'Mol. Biol. Cell 16:4280-4293(2005)', 'Proteomics 5:2227-2237(2005)', 'Cell 127:635-648(2006)', 'J. Biol. Chem. 281:16453-16461(2006)', 'Nat. Biotechnol. 24:1285-1292(2006)', 'Mol. Cell 31:438-448(2008)', 'Proc. Natl. Acad. Sci. U.S.A. 105:10762-10767(2008)', 'Mol. Cell. Biol. 29:1834-1854(2009)', 'Mol. Cell. Proteomics 8:1751-1764(2009)', 'Science 325:834-840(2009)', 'J. Biol. Chem. 285:34062-34071(2010)', 'Sci. Signal. 3:RA3-RA3(2010)', 'BMC Syst. Biol. 5:17-17(2011)', 'Mol. Cell. Proteomics 10:M111.012658.01-M111.012658.12(2011)', 'Mol. Pharmacol. 79:400-410(2011)', 'Sci. Signal. 4:RS3-RS3(2011)', 'Biochim. Biophys. Acta 1820:1839-1848(2012)', 'J. Proteome Res. 12:260-271(2013)', 'J. Proteomics 96:253-262(2014)', 'Mol. Cell. Proteomics 13:372-387(2014)', 'Nat. Struct. Mol. Biol. 21:927-936(2014)', 'Proc. Natl. Acad. Sci. U.S.A. 111:12432-12437(2014)', 'Nat. Struct. Mol. Biol. 24:325-336(2017)', 'Proc. Natl. Acad. Sci. U.S.A. 100:6063-6068(2003)']

As another example, let's say we want to retrieve the organism classifications, that is, the terms that appear separated by semicolons in the lines starting with "OC". This cannot be done with a single regular expression (because a capturing group will always only return the last occurrence of the corresponding match, but we want to get all), so we need to get the OC lines first end then get the individual organism classifications from there. In Python:_____no_output_____

oc_lines = re.findall("(?m)^OC\W+(.+)$", dbentry)
organism_classifications = []
for oc_line in oc_lines:
    ocs = re.findall("\w+", oc_line)
    organism_classifications += ocs

print(organism_classifications)['Eukaryota', 'Metazoa', 'Chordata', 'Craniata', 'Vertebrata', 'Euteleostomi', 'Mammalia', 'Eutheria', 'Euarchontoglires', 'Primates', 'Haplorrhini', 'Catarrhini', 'Hominidae', 'Homo']

For further details on regular expressions (there are a number of things that have not been covered here), see the documentation of the re package at https://docs.python.org/3/library/re.html or the Regular Expression How To at https://docs.python.org/3/howto/regex.html._____no_output_____## Advanced String Manipulation with Regular Expressions

As mentioned above, ```re.findall()``` is not the only useful function when working with regular expressions. Again, the ```re``` module documentation online contains full reference about the available functions. We will look at a few concrete examples here.

The ```re.match()``` and ```re.fullmatch()``` functions can be used to test if a string matches a given pattern. For ```match()```, the beginning of the string must match, while for ```fullmatch()``` the complete string needs to match the pattern. In case of a match, a match object is returned, and None otherwise:_____no_output_____

import re

pattern_06nummer = "06[1-5][1-9][0-9]{6}"

# function re.match
matcher = re.match(pattern_06nummer, "0635436843")
print(matcher)
matcher = re.match(pattern_06nummer, "0635436843xyz")
print(matcher)

# function re.fullmatch
matcher = re.fullmatch(pattern_06nummer, "0635436843")
print(matcher)
matcher = re.fullmatch(pattern_06nummer, "0635436843xyz")
print(matcher)<re.Match object; span=(0, 10), match='0635436843'>
<re.Match object; span=(0, 10), match='0635436843'>
<re.Match object; span=(0, 10), match='0635436843'>
None

Obviously, the match object contains information about the match, such as the begin and end index of the match in the string, and the matched sequence. Accordingly, it provides a number of frequently useful functions and attributes (see https://docs.python.org/3/library/re.html#match-objects). And it has the truth value True itself (while None has the truth value False), so it can easily be used to check if pattern matches the string:_____no_output_____

# function re.match
matcher = re.match(pattern_06nummer, "0635436843")
if matcher:
    print("Match found!")
else:
    print("No match found.")

matcher = re.match(pattern_06nummer, "0635436843xyz")
if matcher:
    print("Match found!")
else:
    print("No match found.")

# function re.fullmatch
matcher = re.fullmatch(pattern_06nummer, "0635436843")
if matcher:
    print("Match found!")
else:
    print("No match found.")

matcher = re.fullmatch(pattern_06nummer, "0635436843xyz")
if matcher:
    print("Match found!")
else:
    print("No match found.")Match found!
Match found!
Match found!
No match found.

The ```re.search()``` function can be used to find the first (if any) occurrence of a pattern in a string. For example, the first occurrence of "wood", "chuck" or "woodchuck" in the woodchuck tongue twister:_____no_output_____

matcher = re.search("wood|chuck|woodchuck", text)
print(matcher)<re.Match object; span=(9, 13), match='wood'>

As a last example, ```re.sub()``` can be used to replace all occurrences of a pattern in a string by something else. For instance, all occurrences of "wood", "chuck" or "woodchuck" by "gotcha!":_____no_output_____

newtext = re.sub("wood|chuck|woodchuck", "gotcha!", text)
print(newtext)How much gotcha! would a gotcha!gotcha! gotcha! if a gotcha!gotcha! could gotcha! gotcha!?

## Exercises
Please use Quarterfall to submit and check your answers. _____no_output_____### 1. Understanding Regular Expressions (★★★★☆)
A. What is described by the following regular expressions?
1. `([01]?[0-9]|2[0-3]):[0-5][0-9]`
2. `[^\s]+\.py$`
3. `[^\s]+(\.(?i)(jpg|png|gif|bmp))$`
4. `[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}`
5. `<tag\b[^>]*>(.*?)</tag>`

B. The format of accession numbers in the UniProt database can be described by the regular expression below. What does it mean? Give 3 examples of valid accession numbers.

`[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2}`

Answer: UniProt accession numbers can either consist of one of the letters O, P and Q followed by a digit (0-9), followed by three letters or digit, and closing with a digit, or of one of the letters A-N or R-Z followed by a digit, followed by one or two times a letter followed by two letters or digits and one digit. _____no_output_____### 2. Writing Regular Expressions (★★★★☆)
Write regular expressions that describe the following:
1. Single-digit and two-digit numbers greater than zero
2. Two-and-more-digit numbers greater than zero
3. Single-and-more-digit numbers greater than zero
4. Four-digit numbers greater than zero
5. Four-and-more digit numbers greater than zero
6. Two-to-four-digit numbers greater than zero
7. Floating-point numbers between 0 and 1 (including)
8. Temperatures (C or F) with 1 decimal place
9. Dutch postal codes
10. BSN numbers_____no_output_____### 3. Information from a Database Entry (★★★★☆)
Download the plain UniProt database entry that we already used in the lecture (https://www.uniprot.org/uniprot/P05787.txt). Write a Python program that retrieves and prints the creation and modification dates from the DT lines, and the keywords from the KW lines. You can use the following code to read the content of the file into a string variable dbentry:
```
with open("P05787.txt", "r") as file:
    dbentry = file.read()
```

The output should be something like:
```
['01-NOV-1988', '23-JAN-2007', '13-FEB-2019']
['Acetylation', 'Alternative splicing', 'Coiled coil', 'Cytoplasm', 'Direct protein sequencing', 'Disease variant', 'Glycoprotein', 'Host-virus interaction', 'Intermediate filament', 'Isopeptide bond', 'Keratin', 'Methylation', 'Nucleus', 'Phosphoprotein', 'Polymorphism', 'Reference proteome', 'Ubl conjugation']
```_____no_output_____### 4. String Reformatting (★★★★☆)
Write a Python program that reads the same input file as in the previous exercise, anonymizes the authors in the RA lines, and prints the modified entry to the screen. The output should be something like:
```
ID   K2C8_HUMAN              Reviewed;         483 AA.
AC   P05787; A8K4H3; B0AZN5; F8VXB4; Q14099; Q14716; Q14717; Q53GJ0;
AC   Q6DHW5; Q6GMY0; Q6P4C7; Q96J60;
[...]
RP   ARG-47, AND IDENTIFICATION BY MASS SPECTROMETRY [LARGE SCALE
RP   ANALYSIS].
RC   TISSUE=Colon carcinoma;
RX   PubMed=24129315; DOI=10.1074/mcp.O113.027870;
RA   (authors anonymized)
RA   (authors anonymized)
RA   (authors anonymized)
RT   "Immunoaffinity enrichment and mass spectrometry analysis of protein
RT   methylation.";
[...]
```_____no_output_____## Extra
Fancy something more geeky with regular expressions? Try the RegEx crossword puzzles at https://regexcrossword.com/!_____no_output_____


# Notebook from gtbook/robotics
Path: S44_logistics_perception.ipynb

_____no_output_____

%pip install -q -U gtbookNote: you may need to restart the kernel to use updated packages.
import numpy as np
from numpy.random import default_rng
rng = default_rng()

import plotly.express as px
import plotly.graph_objects as go
try:
    import google.colab
except:
    import plotly.io as pio
    pio.renderers.default = "png"

import gtsam
from gtbook import logistics
from gtbook.display import show_____no_output_____

# Localization

> We introduce three instances of Bayes filtering: Kalman filters, Markov localization, and Monte Carlo localization.

**This Section is still in draft mode and was released for adventurous spirits (and TAs) only.**_____no_output_____

from gtbook.display import randomImages
from IPython.display import display
display(randomImages(4, 4, "steampunk", 1))_____no_output_____

The first order of business is creating a small ground truth trajectory. Rather than using simulation we generate it deterministically here, so ew can easily assess the quality of our solution. In particular, let us start at and move to the right, then up between the middle two shelves:_____no_output_____

left = [(10+i*5,6) for i in range(9)]
up = [(50,6+i*5) for i in range(1,6)]
N = len(left) + len(up)
indices = range(1, N+1)
x = {k:gtsam.symbol('x',k) for k in indices}
values = gtsam.VectorValues()
for i, state in enumerate(left+up): values.insert(x[i+1], state)
values_____no_output_____

Note above we used a `gtsam.VectorValues` to store the ground truth trajectory, which will come in handy again when we simulate the measurements. Below we show this "ground truth" trajectory overlaid on the warehouse map we introduced before:_____no_output_____

ground_truth = np.array([values.at(x[k]) for k in indices])
logistics.show_map(logistics.base_map, ground_truth)_____no_output_____

## Markov Localization

> Complicated sensors necessitate expressive density representations.

Through finite element discretization we can use the sum-product algorithm from the previous chapter, as is. When applied to robot localization, as we are using a discrete Markov chain representation, this approach has been called **Markov Localization**. However, because of the computational expense involved, we will not be able to run the sum-product algorithm over *multiple* times steps. Instead of asking for the full posterior over an entire sequence, we will have to be content with the posterior on the *current state* only. This approach is called *filtering*, and this is where we start below._____no_output_____### The Bayes Filter

> The Bayes filter is a recursive, ons-step sum-product algorithm.

The **Bayes filter** is a simple inference scheme that recursively implements the sum-product algorithm.
In this case we present the discrete version, which will yield Markov localization.
However,  the same general scheme underlies the venerable Kalman filter, and Monte Carlo Localization, which is based on a sampling-based density representation.

We assume a finite element discretization such that a density $p(x_k)$ over continuous states $x_k$ is approximated by a discrete probability distribution $P(X_k)$. The only difference from the previous chapter, where we discussed discreet state spaces, is that in a finite element discretization the cardinality of the discrete states is typically much larger. Recall from section 4.1 that for our $100m \times 50m$ warehouse example, even at a relatively course resolution of 1m by 1 m cells, our state space had a cardinality of 5000 finite elements. That is exactly what makes reasoning over multiple time steps computationally challenging. In other words, we have to give up on computing the joint posterior $P(X_1, X_2, ...X_k)$._____no_output_____Instead, in the Bayes filter we are only interested in estimating the state of the
robot at the *current time* $t$, given all the measurements up to and
including time $t$.
In Bayesian fashion, we attempt at characterizing our complete knowledge of the state $X_k$ by computing its posterior $P(X_k)$, which is also called the **filtering distribution**.
To be very clear, we indicate exactly what information we are
conditioning on when giving the formulas for the Bayes filter. To that
end, we introduce the notation
$\mathcal{Z}^{k}=\{z_{1},z_{2},\ldots z_{k}\}$, i.e., all measurements
$o$ up to and including time $t$.
Similarly, we define the controls
$\mathcal{U}^{k}=\{u_{1},u_{2},\ldots u_{k-1}\}$.
Note there is always one less control variable than there are measurements, and we start off the algorithm with Bayes rule as applied to $k=1$:

$$
P(X_1|\mathcal{Z}^1=\{z_1\}, \mathcal{U}^1=\{\}) \propto L(X_1;z_1)P(X_1)
$$_____no_output_____Given this, recursively assuming that we have a probability distribution
$P(x_{t-1}|\mathcal{Z}^{k-1},\mathcal{U}^{k-1})$ over the previous state $X_{k-1}$, we proceed in two
phases:

1.  In the **prediction phase** we calculate a **predictive distribution**
    $P(X_{k}|\mathcal{Z}^{k-1},\mathcal{U}^{k})$ on the current state $X_{k}$ given a control $u_{k-1}$. This is done by marginalizing out
    the previous state $X_{k-1}$, by summing over all possible values
    $x_{k-1}$ for $X_{k-1}$:
    $$
    P(X_{k}|\mathcal{Z}^{k-1},\mathcal{U}^{k})=\sum_{x_{k-1}}P(X_{k}|x_{k-1},u_{k-1})P(x_{t-1}|\mathcal{Z}^{k-1},\mathcal{U}^{k-1}).
    $$

2.  In the **measurement phase** we upgrade this predictive density to the **filtering distribution** $P(X_{k}|\mathcal{Z}^{k},\mathcal{U}^{k})$
    via Bayes’ rule, given the measurement $z_{k}$:
    $$
    P(X_{k}|\mathcal{Z}^{k},\mathcal{U}^{k}) \propto L(X_{k};z_{k})P(X_{k}|\mathcal{Z}^{k-1},\mathcal{U}^{k}).
    $$

A keen observer will see that step 1 and step 2 above implement one step of the sum-product algorithm, where in this case the previous state $X_{k-1}$ is eliminated. Step 1 is a sum, and step 2 is a product._____no_output_____### A (historically meaningful) 1D Example
Figure
1 below
illustrates the measurement phase for a simple 1D example.
The example is historically meaningful as it was created by Sebastian Thrun to explain Markov Localization over 20 years ago, and we reuse it here again.

One-dimensional example of the measurement phase in Markov localization, the discrete version of the Bayes filter.

In this environment there are two doors, and the robot has a door sensor. The
predictive distribution $P(S)$ encodes that we believe the robot to be
near the left door. The likelihood $L(S;o)$, where $o$ indicates that we
*did* perceive a door, models the fact that we are more likely to be
near a door given $O=o$, but also allows for the fact that the door
sensor could misfire at any location. Note that the likelihood is
un-normalized and there is no need for it to sum up to 1. Finally, the
posterior $P(S|o)$ is obtained, via Bayes’ rule, as the product of the
prediction $P(S)$ and the likelihood $L(S;o)$, and is shown at the
bottom as a normalized probability distribution. As you can see, the
most probable explanation for the robot state is $S=5$, but there is a
second mode at $S=17$ due to the bimodal nature of the likelihood.
However, that second mode is less probable because of our prior belief
over $S$._____no_output_____### Warehouse Example

Let us apply Markov localization to the warehouse example, using *just* the proximity sensor for now. We start by initializing the finite element density representation with a prior, centered around the ground truth location for $k=1$, but with a relatively large standard deviation of 5 meter:_____no_output_____

prior_mean = values.at(x[1])
prior_cov = np.diag([25,25])
prior = logistics.gaussian(logistics.map_coords, prior_mean, prior_cov)
logistics.show_map(prior/np.max(prior)+0.1*logistics.base_map)_____no_output_____

The hardest and most computationally demanding part in the Markov localization algorithm to implement is the prediction step. Recall the formula:

$$
P(X_{k}|\mathcal{Z}^{k-1},\mathcal{U}^{k})=\sum_{x_{k-1}}P(X_{k}|x_{k-1},u_{k-1})P(x_{t-1}|\mathcal{Z}^{k-1},\mathcal{U}^{k-1}).
$$

Hence, for every cell in the predictive density approximation, we need to sum over *all* cells in the previous image. Not only that, but for every one of these $5000^2$, i.e., *25 million* cell combinations $(X_k,x_{k-1})$ we need to evaluate the Gaussian motion model $P(X_{k}|x_{k-1},u_{k-1})$. With python for-loops, this will be rather expensive, so in the code below build in two speedups:
1. We make the outer loop over the previous image, and threshold on the density;
2. we make use of the fact that the `logistics.gaussian` function is vectorized, i.e., we can process an entire row of the predictive image at a time.

The fast code looks like this:_____no_output_____

def prediction_step(previous, control, motion_model_sigma):
    """Calculate predictive density given control and control stddev."""
    cov = np.eye(2) * motion_model_sigma**2
    predictive_density = np.zeros((50,100))
    for i in range(50):
        for j in range(100):
            # Speedup 1: threshold on previous[i,j]
            if previous[i,j]>1e-5:
                previous_xy = logistics.map_coords[i,j]
                mean = previous_xy + control
                for k in range(50):
                    # Speedup 1: vectorize Gaussian evaluation over predictive row:
                    motion_model = logistics.gaussian(logistics.map_coords[k], mean, cov)
                    predictive_density[k] += motion_model * previous[i,j]
    return predictive_density/np.sum(predictive_density)_____no_output_____

If we have *no* measurements at all, but have a perfect measurement model, our "control tape" just pushes the Gaussian density along:_____no_output_____

%%time
motion_model_sigma = 2
current_density = prior
for k in indices[:-1]:
    # prediction phase
    control = values.at(x[k+1]) - values.at(x[k]) # ground truth control
    current_density = prediction_step(current_density, control, motion_model_sigma)
# logistics.show_map(current_density/np.max(current_density) + 0.1*logistics.base_map)CPU times: user 18.1 s, sys: 1.64 s, total: 19.7 s
Wall time: 12.4 s

Evolution of the predictive density (no measurements).

The figure above shows the result of purely predictive reasoning using a finite element discretization. As you can see, the density grows without bounds, and also goes *inside* the shelves. Without any measurements, we really do not know any better.

The measurement update step takes the predictive density $P(X_{k}|\mathcal{Z}^{k-1},\mathcal{U}^{k})$ and *upgrades* it to the posterior density:

$$
P(X_{k}|\mathcal{Z}^{k},\mathcal{U}^{k}) \propto L(X_{k};z_{k})P(X_{k}|\mathcal{Z}^{k-1},\mathcal{U}^{k}).
$$

Note that this is so much simpler! We just have to code a pointwise multiplication and we are done. Because the ground truth trajectory never comes near and of the shelves, the proximity sensor is always `OFF`, and hence we multiply the predictive density with the corresponding `proximity_map_off` likelihood image:_____no_output_____

%%time
predictive_density = prior
posterior_density = predictive_density * logistics.proximity_map_off
posterior_density /= np.sum(posterior_density)
for k in indices[:-1]:
    # prediction phase
    control = values.at(x[k+1]) - values.at(x[k]) # ground truth control
    predictive_density = prediction_step(posterior_density, control, motion_model_sigma)
    # measurement update phase
    posterior_density = predictive_density * logistics.proximity_map_off
    posterior_density /= np.sum(posterior_density)
# logistics.show_map(posterior_density/np.max(posterior_density) + 0.1*logistics.base_map)CPU times: user 6.75 s, sys: 691 ms, total: 7.44 s
Wall time: 4.65 s

Markov localization in action.

The figure above shows Markov localization in action! The measurement information has *literally* squeezed the predictive density to a posterior that fuses predictive information with the knowledge that we cannot be anywhere near the shelves, or the wall. Note that the density is still spreading in the direction that the sensor does *not* yield information.
_____no_output_____#### Exercises
- In principle our motion model should have told us that we cannot move inside shelves. How would you change the prediction code to make this so?
- The full Markov localization algorithm takes *less* time than the code *without* the measurement update. We do more work yet have faster inference. Why?_____no_output_____## Monte Carlo Localization

> Localization with a particle-filter is known as Monte Carlo Localization.

Samples are cheaper, faster, and better. The above finite element discretization of space is very costly, and most of the memory and computation is used to compute near-zero probabilities.
While there *are* ways to deal with this, switching to a sampling-based representation gets us more bang for the buck, computation-wise. And, as we will see, it also leads to a very simple algorithm.
The sampling-based implementation of a Bayes filter is known as a **particle filter**.
Below we discuss it in a 2D context, but it is in fact rather general.
When used for robot localization, this technique is known as ["Monte Carlo Localization" or MCL](https://www.ri.cmu.edu/publications/monte-carlo-localization-for-mobile-robots/)._____no_output_____A particle filter approximates the Bayes filter by (1) replacing an explicit probability mass function over finite elements with a set of samples, and (b) approximating the prediction step in the Bayes filter with a Monte Carlo approximation. We recursively assume to have access to a sampling based approximation of the filtering density over the previous state $x_{k-1}$, as a set of $S$ *weighted* samples:

$$
p(x_{t-1}|\mathcal{Z}^{k-1},\mathcal{U}^{k-1}) \approx \{x_{k-1}^{(s)}, w_{k-1}^{(s)}\}.
$$_____no_output_____We then implemented proceed in two phases:

1.  In the prediction phase we approximate the predictive distribution
    $p(x_{k}|\mathcal{Z}^{k-1},\mathcal{U}^{k})$ by drawing $S$ *unweighted* samples from the following **mixture density** of motion model densities:
    $$
    \{ x_k^{(t)} \} \sim \sum_s w_{k-1}^{(s)} P(x_{k}|x_{k-1}^{(s)},u_{k-1})P(x_{t-1}^{(s)}|\mathcal{Z}^{k-1},\mathcal{U}^{k-1}).
    $$

2.  In the measurement phase we upgrade this predictive density to a weighted sample approximation of the filtering distribution, by weighting each unweighted sample with the likelihood of $x_k^{(t)}$ given the measurement $z_{k}$:
    $$
    p(x_{t-1}|\mathcal{Z}^{k-1},\mathcal{U}^{k-1}) \approx \{x_k^{(t)}, L(x_k^{(t)};z_{k})\}.
    $$

We start off the algorithm with a set of $S$ samples from the prior, weighted by the likelihood on the first $x_1$ given the first measurement $z_1$:

$$
P(X_1|\mathcal{Z}^1=\{z_1\}, \mathcal{U}^1=\{\}) \approx \{x_1^{(t)}, L(x_1^{(t)};z_1)\}~\text{with}~x_1^{(t)}\sim p(x_1)
$$

In the "vanilla" particle filter outlined above, the number of samples stays constant over time, but variants exist that adapt the number of samples to the complexity of the density over time, or even to the available computation. Used in that way, a particle filter can be used as a "just in time" algorithm._____no_output_____###  The Prediction Step
Again, the prediction phase is the most complex. A mixture density is simply a weighted sum of *component densities*, in this case a set of motion models. The weights are a probability mass function over the component indices and hence should sum to 1. Sampling from a mixture density proceeds in two steps:
1. Pick a component according to the weight distribution;
2 sample from the component.

As a toy example, let us assume a *Gaussian mixture density* with just two Gaussian components, respectively with weights 0.9 and 0.1:_____no_output_____

means = [gtsam.Point2(x,y) for x,y in [(20,25),(70,40)]]
covariances = [np.diag([sx**2,sy**2]) for sx,sy in [(5,10),(20,5)]]
data = [go.Contour(z=logistics.gaussian(logistics.map_coords, mean, cov), contours_coloring='lines',
        line_width=2, showscale=False) for mean,cov in zip(means,covariances)]
fig = go.Figure(data=data); fig.show()_____no_output_____

The following code samples 100 samples, rather inefficiently:_____no_output_____

component_indices = rng.choice(2, size=1000, p=[0.9,0.1])
samples = np.array([rng.multivariate_normal(means[s], covariances[s]) for s in component_indices])
fig.add_trace(go.Scatter(x=samples[:,0],y=samples[:,1], mode="markers",
        marker=dict(size=3, color="red")))_____no_output_____

As you can see the first component is chosen much more often, which is exactly what we expected.

Armed with this general knowledge about sampling mixtures, we apply it below to code up the MCL prediction step:_____no_output_____

def predict_samples(samples, weights, control, motion_model_sigma, size=500):
    """Create predictive density from weighted samples given control and control stddev."""
    weights /= np.sum(weights)
    component_indices = rng.choice(len(samples), size=size, p=weights)
    means = samples + control
    cov = np.eye(2) * motion_model_sigma**2
    return np.array([rng.multivariate_normal(means[s], cov) for s in component_indices])_____no_output_____

### Evaluating the Likelihood

As before, we know that the ground truth trajectory stays away from obstacles at all times. However, we only have access to a likelihood *image*, not a function that evaluates the likelihood for an arbitrary coordinate. The following function calculates the correct cell for looking up the likelihood, and returns 0 likelihood if out of bounds:_____no_output_____

def likelihood_off(xy):
    """Calculate likelihood by looking up value in proximity_map_off."""
    j, i = np.round(xy).astype(int)
    if i<0 or i>49 or j<0 or j>99: return 0.0
    return float(logistics.proximity_map_off[i,j])_____no_output_____

### MCL Warehouse Example

Now that we know how to do the prediction step and evaluate the likelihood function, we are able to implement the entire Monte Carlo localization method. First, let us obtain 500 samples from the posterior:_____no_output_____

S=500
prior_samples = rng.multivariate_normal(prior_mean, prior_cov, size=S)
logistics.show_map(0.1*logistics.base_map, markers=prior_samples,
         marker=dict(size=3,color="red"))_____no_output_____

Compare this sampling-based representation with the Markov localization representation of the prior above, and note they represent the *same* density, but using vastly different representations. In fact, they also use vastly different resources: for Markov localization we used 5000 cells, and here we use 500 two-dimensional vectors. In higher-dimensional state spaces this difference is even greater: what would happen if we wanted to represent orientation as well?

However, as discussed, we start off MCL with a set of *weighted* samples $\{x_1^{(t)}, L(x_1^{(t)};z_1)\}$, which represents the posterior for $k=1$. We do this by weighting each sample from the prior with the likelihood:_____no_output_____

samples = prior_samples
weights = np.apply_along_axis(likelihood_off, 1, samples)
logistics.show_map(0.1*logistics.base_map, markers=samples,
         marker=dict(color="red", opacity=0.1, size=10*weights/np.max(weights)))_____no_output_____

Note that all samples close to the wall and/or the shelves have disappeared, as they have zero weight. All remaining samples have the same weight._____no_output_____

%%time
for k in indices[:-1]:
    # prediction phase
    control = values.at(x[k+1]) - values.at(x[k])  # ground truth control
    samples = predict_samples(samples, weights, control, motion_model_sigma)
    # measurement update phase
    weights = np.apply_along_axis(likelihood_off, 1, samples)
# logistics.show_map(0.1*logistics.base_map, markers=samples,
#          marker=dict(color="red", opacity=0.1, size=10*weights/np.max(weights)))CPU times: user 185 ms, sys: 2.76 ms, total: 188 ms
Wall time: 188 ms

Monte Carlo localization in action.

The figure above shows Monte Carlo localization in action! Comparing with Markov localization, we see that the results are consistent. Not only that, but if you look at the timing numbers, MCL runs at least an order of magnitude faster.

You might wonder about the samples that made it into the seemingly *wrong* aisles between the shelves. However, if we crank up the Markov localization visualization by a factor of 10, you will see that *in* fact the probability of being in those aisles is actually non-zero:_____no_output_____

logistics.show_map(10*posterior_density/np.max(posterior_density) + 0.1*logistics.base_map)_____no_output_____

MCL can be made to be *more* accurate than Markov localization, as in the limit an infinite number of samples will represent the actual densities exactly. Of course, we can also increase the resolution of the Markov localization representation, but at exponential cost. In contrast, the samples needed are proportional to the "volume" that the density occupies, an observation that underlies adaptive variants._____no_output_____### Adding Range Sensing

Finally, and this is will be rather easy in MCL, let us investigate the effect of adding range sensing. This time we won't have the luxury to know the entire measurement sequence in advance, as we did with proximity. Rather, the likelihood will have to be given the output of the RFID sensor. Remember that was either a beacon ID and a range $(i,r)$, or $(\text{None},\infty)$._____no_output_____

def likelihood_range(xy, rfid_measurement):
    """Calculate likelihood of xy given range measurement."""
    _id, _range = rfid_measurement
    if _id is None:
        j, i = np.round(xy).astype(int)
        if i < 0 or i > 49 or j < 0 or j > 99:
            return 0.0
        return float(logistics.out_of_bound_map[i, j])
    else:
        sigma = 1.0  # In meters
        range_for_xy = logistics.rfid_range(xy, logistics.beacons[_id])
        return 0.0 if range_for_xy is None else np.exp(-1/(2 * sigma**2) * (range_for_xy - _range)**2)_____no_output_____

We can also use this to illustrate how to *globally* localize, without any prior information at all, except that we are *somewhere* in the warehouse. We can encode this knowledge as samples by uniformly sampling over the entire range for $x$ and $y$. Because our knowledge is now so diffuse, we use 2000 samples instead of 500 to better approximate it:_____no_output_____

T=2000
prior_samples = rng.uniform(low=gtsam.Point2(0,0), high=gtsam.Point2(100,50), size=(T,2))
logistics.show_map(0.1*logistics.base_map, markers=prior_samples,
         marker=dict(size=3,color="red"))_____no_output_____

The first range measurement happens to be out if range, and we can see the effect on our estimate for the posterior $p(x_1|z_1)$ quite clearly:_____no_output_____

samples = prior_samples
range_measurement = logistics.rfid_measurement(values.at(x[1]))
weights = np.apply_along_axis(likelihood_range, 1, samples, range_measurement)
logistics.show_map(0.1*logistics.base_map, markers=samples,
         marker=dict(color="red", opacity=0.1, size=10*weights/np.max(weights)))_____no_output_____

As expected, the likelihood has "punched holes" around the beacons, because we *know* we cannot have been near them. This is the power of *negative* information. When we combine it with the proximity OFF measurement, we narrow our posterior down even more:_____no_output_____

weights *= np.apply_along_axis(likelihood_off, 1, samples)
logistics.show_map(0.1*logistics.base_map, markers=samples,
         marker=dict(color="red", opacity=0.1, size=10*weights/np.max(weights)))_____no_output_____

We are now ready to bring it all together, now multiplying *two* likelihoods in ever measurement update phase:_____no_output_____

%%time
for k in indices[:-1]:
    # prediction phase
    control = values.at(x[k+1]) - values.at(x[k])  # ground truth control
    samples = predict_samples(samples, weights, control, motion_model_sigma)
    # measurement update phase
    range_measurement = logistics.rfid_measurement(values.at(x[k]))
    weights = np.apply_along_axis(
        likelihood_range, 1, samples, range_measurement)
    weights *= np.apply_along_axis(likelihood_off, 1, samples)
# logistics.show_map(0.1*logistics.base_map, markers=samples,
#          marker=dict(color="red", opacity=0.1, size=10*weights/np.max(weights)))CPU times: user 204 ms, sys: 2.93 ms, total: 207 ms
Wall time: 208 ms

Global localization using MCL aided by range sensing.

The figure above shows global localization in action! At the onset, we are very confused as to where we might be in the warehouse, but we still have *some* knowledge. Then we move within range of beacon $0$, which immediately localizes us: it acts a bit like a GPS sensor, especially in combination with what we sensed before. After that the particular shape of the range likelihoods makes for some interesting antics, and at the end we are quite a bit more certain that we are in the center aisle.

This example showed off *two* really nice properties of Monte Carlo localization (and, for that matter, Markov localization):
1. We can reason about multi-modela densities and even the complete absence of knowledge.
2. We can incorporate powerful, *negative* information, such as out of range measurements.

To emphasize the latter point even more, note that for the last few time steps in the animation above we are *out* of beacon range, and you can see a bit of "squeezing" of the sample cloud at the very top, because of this. Negative information for the win!_____no_output_____## Kalman Smoothing and Filtering

> Where Gaussians are used, Kalman was there first.

The final and mathematically most challenging approach we will discuss is Kalman smoothing and filtering. Ironically, while the math is harder, the circumstances in which these methods can be applied are more limited.
The math is only exact for (a) linear measurement models and (b) Gaussian additive noise.
In addition, although there are ways to deal with nonlinear sensors, a more serious limitation is that the density on the state can only ever be Gaussian itself.

These methods are incredibly important in robotics, however, For all their limitations, Kalman smoothers and filters are incredibly efficient, and are at the core of the navigation stack of many systems, robots and otherwise. In fact, the Kalman filter was developed in order to perform navigation on board of military aircraft in the sixties, and were able to run on the extremely limited computational hardware - by today's standards - on board those aircraft.
_____no_output_____### Factor Graphs and Least Squares_____no_output_____> When given measurements and actions, we create a factor graph directly.

**Kalman smoothing** is the equivalent of the Viterbi algorithm for hidden Markov model, but for continuous state spaces.

In the continuous case, we build the factors directly from given controls and measurements. A factor graph now encodes the negative log-posterior

$$\Phi(X)=\sum_i \phi(X_i) = \frac{1}{2} \sum_i \|A_i X_i-b_i\|^2.$$

In the continuous case we use *minimization* of the log-likelihood rather than maximization over the probabilities. The main reason is because then inference becomes a linear least squares problem._____no_output_____A least-squares problem is convex and can be solved in closed form. The linear terms within the square norm above are *sparse*, in the sense that $X_i$ is only a *subset* of the continuous variables $X$. For example, a simple sensor fusion problem on two scalar variables $x_1$ and $x_2$ would be one where both have a simple prior on them, resp. with $\mu_1=3$ and $\mu_2=7$, and we know that the difference between them is equal to $5$. This yields three factors that have to be minimized over $x_1$ and $x_2$:

$$
\Phi(X)= \frac{1}{2} \|x_1-3\|^2 + \frac{1}{2} \|(x_2-x_1)-5\|^2 + \frac{1}{2} \|x_2-7\|^2.
$$

This corresponds to the following

|$X_i$|$A_i$|$b_i$|
|-|-|-|
|$x_1$|[1]|3|
|$x_1, x_2$|[-1 1]|5|
|$x_2$|[1]|7|_____no_output_____We can collect all three matrices $A_1$, $A_2$, and $A_3$ into one sparse matrix $A$ and corresponding right-hand-side $b$:

$$A=\begin{pmatrix} 1 & 0 \\ -1 & 1 \\ 0 & 1 \end{pmatrix}~~\text{and}~~B=\begin{pmatrix} 3 \\ 5 \\ 7 \end{pmatrix}.$$

You can convince yourself that the minimization problem can then be written as

$$X^* = \arg \min_X \Phi(X) = \arg \min_X \frac{1}{2} \|A X-b\|^2$$

which is now a *sparse* linear least-squares problem._____no_output_____Setting the derivative to zero and solving for $X^*$ we obtain:

$$\begin{align*}
A^T(A X^* - b) &= 0 \\
(A^T A) X^* &= A^T b \\
X^* &= (A^T A)^{-1} A^T b
\end{align*}$$

This is known as a **system of normal equations**, and the last equation above is a closed from solution for it.

Solving a linear least-squares problem this way can be expensive. The matrix $A$ can be very high dimensional, but luckily in many sensor fusion scenarios it is exceedingly *sparse*. Still, we have to proceed with some care, as in general the matrix $(A^T A)^{-1}$ is *not* sparse. The matrix $Q \doteq A^T A$ is known as the **Hessian** or **Information Matrix** and has dimensions $n\times n$ if there are $n$ scalar unknowns. In general, it takes $O(n^3)$ work to compute it, which is only 27 for $n=3$, but is already 1 *billion* for $n=1000$._____no_output_____### Numerical Example
It is trivial to implement in numpy for the small example above:_____no_output_____

A,b = np.array([[1.,0.],[-1.,1.],[0.,1.]]), np.array([3.,5.,7.])
X_optimal = np.linalg.inv(A.T @ A) @ A.T @ b
print(X_optimal)[2.66666667 7.33333333]

Note that in this simple sensor fusion example, a compromise solution is obtained: all factors are equally *unhappy*. We can compute the error vector as $A X^* - b$ and find that the errors is equally divided between all factors:_____no_output_____

print(A @ X_optimal - b)[-0.33333333 -0.33333333  0.33333333]

#### Exercise

What would make the errors *not* divide equally between factors?_____no_output_____### Sparse Least-Squares

In practice we use *sparse factorization methods* to solve for $X^*$. In particular, *sparse Cholesky* factorization can efficiently decompose the sparse Hessian $Q$ into its matrix square root $R$

$$A^T A=R^T R$$

where $R$ is *upper-triangular* and sparse as well. There is one wrinkle: the sparsity of $R$ depends (dramatically) on the column ordering chosen for $A$, and the optimal ordering is hard to find (NP-complete, in fact). Hence, many heuristic ordering methods exist that are applied in practice.

After we found $R$, which is where the heavy lifting occurs, we can efficiently solve for $X^*$ in two steps. First, we solve for an auxiliary vector $y$:

$$\begin{align*}
(R^T R) X^* &= A^T b \\
R^T (R X^*) &= A^T b \\
R^T y &= A^T b
\end{align*}$$

In the last line, $R^T$ is lower-triangular, and hence $y$ can be easily solved for proceeding from $y_1$ to $y_n$, via *forward-substitution*. We then turn around use the computed value of $y$ to solve for $X^*$ by *back-substitution*:

$$R X^*=y.$$

Both these steps are $O(n^2)$ for dense matrices, but typically closer to $O(n)$ for sparse matrices.

There is a deep connection between sparse factorization methods and the sum-product algorithm discussed in the previous chapter. In fact, sparse factorization *is* sum-product, where one continuous variable is eliminated at a time. The factor graph corresponding to a sparse least squares problem correspond to its sparsity pattern. In this graphical framework, the *product* is implemented by collecting all factors connected to that variable, and the *sum* is implemented by expressing the variable to be eliminated as a linear combination of its neighbors in the graph._____no_output_____### Example with GPS-Like Measurements

> We build a factor graph and then simply call optimize.

GTSAM is built around state of the art sparse factorization methods, and hence can very efficiently solve large sensor fusion problems, like the localization problem with GPS-like measurements.
Below we illustrate this with an example in code for the GPS-like measurements, which is easily tackled using optimization. Range measurements can also be handled, but *out of range* measurements are not so easy, so we will defer handling range to the next section._____no_output_____We simulate measurements using the ground truth trajectory but with noise added, by creating a quick Bayes net do this for us. Remember from the previous section that the measurements are in centimeters, so the number $30$ below is the standard deviation of the measurement noise, in centimeters:_____no_output_____

C = 100 * np.eye(2)
measurement_model_sigma = 30
z = {k: gtsam.symbol('z', k) for k in indices}
bn = gtsam.GaussianBayesNet()
for k in indices:
    conditional_on_zk = gtsam.GaussianConditional.FromMeanAndStddev(
        z[k], C, x[k], [0, 0], measurement_model_sigma)
    bn.push_back(conditional_on_zk)
simulation = bn.sample(values)
print(f"measurement on {values.at(x[1])} = {simulation.at(z[1])}")
measurement on [10.  6.] = [980.21898282 565.87931563]

Notice how the measurement is scaled by 100, and is corrupted by noise. Now we are finally ready to create a GaussianFactorGraph that will fuse the information from the prior, the measurements, and the motion models:_____no_output_____

gfg = gtsam.GaussianFactorGraph()

# The prior
p_x1 = gtsam.GaussianDensity.FromMeanAndStddev(x[1], values.at(x[1]), 0.5)
gfg.push_back(p_x1)

# Create motion model factors from ground truth trajectory displacements
for k in indices[:-1]:
    state = values.at(x[k])
    next_state = values.at(x[k+1])
    displacement = next_state - state
    # |next_state - (A*state + B*u)|
    gfg.push_back(gtsam.GaussianConditional.FromMeanAndStddev(
        x[k+1], np.eye(2), x[k], displacement, motion_model_sigma))

# Convert the conditionals in Bayes net from above into likelihood factors
for k in indices:
    conditional_on_zk = bn.at(k-1)
    measurement = simulation.at(z[k])
    likelihood_on_xk = conditional_on_zk.likelihood(measurement)
    gfg.push_back(likelihood_on_xk)_____no_output_____

Below we show the factor graph, which looks exactly like the one for inference in an HMM, except we now have more states, and they are continuous. The two problems have essentially the same computational *structure*:_____no_output_____

position_hints = {'x': 1}
pos = {0:(0.35,1)}
pos.update({k:(k+0.5,1) for k in indices[:-1]}) # binary
pos.update({k+N-1:(k,0.5) for k in indices}) # unary
show(gfg, hints=position_hints, factor_positions=pos)_____no_output_____

Now we can find the optimal solution, using the `optimize` method. In the Gaussian factor graph case, this solves a linear least-squares problem with a continuous variant of the sum-product algorithm:_____no_output_____

least_squares_solution = gfg.optimize()_____no_output_____

Below we compare the estimated trajectory with the ground truth trajectory:_____no_output_____

estimated = np.array([least_squares_solution.at(x[k]) for k in indices])
logistics.show_map(logistics.base_map, estimated)_____no_output_____

## GTSAM 102

> A deeper dive in the GTSAM concepts used above._____no_output_____Recall the `gtsam.GaussianFactorGraph` `gfg`:_____no_output_____

show(gfg, hints=position_hints, factor_positions=pos)_____no_output_____

We can also visualize the sparse $A$ matrix, which is called the **sparse Jacobian** of the system:_____no_output_____

# show [A|b]
A, b = gfg.jacobian()
px.imshow(np.abs(A), color_continuous_scale='Reds')
_____no_output_____

In fact, the deep connection between factor graphs and sparse linear algebra is this:
> Columns correspond to variables, and rows correspond to factors

You can make out, from the top:
- the first two rows correspond to the prior
- the two darker diagonals correspond to the motion model factors
- the single red diagonal corresponds to the measurements

_____no_output_____We can use the method `eliminateSequential` to use the linear-Gaussian sum-product algorithm (matrix factorization!) to turn it into a Bayes net, which encodes the multivariate Gaussian posterior:_____no_output_____

gaussian_posterior = gfg.eliminateSequential()
show(gaussian_posterior, hints=position_hints)_____no_output_____

This Bayes net *is* the upper-triangular (DAG!) Cholesky factor $R$ we discussed above:_____no_output_____

R, d = gaussian_posterior.matrix()
display(px.imshow(np.abs(R), color_continuous_scale='Reds'))_____no_output_____

Indeed, here the correspondence is:
> columns correspond to variables, and rows correspond to Gaussian conditionals.

In general, the correspondence is about *block* columns, as vraiables in this case are two-dimensional, and *block* rows. For example, the first `GaussianConditional` in the `gaussian_posterior` Bayes net corresponds to the first two rows above, and can be shown like so:_____no_output_____

gaussian_posterior.at(0).print("First two rows are: ")First two rows are:   Conditional density [x1]
  R = [ 3.91933       0 ]
      [       0 3.91933 ]
  S[x2] = [ -0.0637865          0 ]
          [          0 -0.0637865 ]
  d = [ 37.6757 22.1659 ]
  No noise model


# Notebook from hrshdhgd/ontoSpacy
Path: notebooks/SpacyWithPaper.ipynb

!python -m spacy info/opt/anaconda3/envs/ontospacy/lib/python3.9/site-packages/spacy/util.py:833: UserWarning: [W095] Model 'en_core_web_lg' (3.0.0) was trained with spaCy v3.0 and may not be 100% compatible with the current version (3.2.3). If you see errors or degraded performance, download a newer compatible model or retrain your custom model with the current spaCy version. For more details and available updates, run: python -m spacy validate
  warnings.warn(warn_msg)

============================== Info about spaCy ==============================

spaCy version    3.2.3
Location         /opt/anaconda3/envs/ontospacy/lib/python3.9/site-packages/spacy
Platform         macOS-10.16-x86_64-i386-64bit
Python version   3.9.7
Pipelines        en_ner_craft_md (0.5.0), en_core_web_sm (3.2.0), en_core_web_lg (3.0.0), en_core_sci_scibert (0.5.0)

%reset -f_____no_output_____# One time run.
# ! python -m spacy download en_core_web_sm_____no_output_____

## Imports_____no_output_____

from spacy.language import Language
from spacy.pipeline import entityruler
import pandas as pd
import os
import spacy
from collections import defaultdict
from scispacy.linking import EntityLinker
from spacy.tokens import Doc, Span, Token
from spacy.matcher import PhraseMatcher
from spacy import displacy
from dframcy import DframCy
import pickleYour CPU supports instructions that this binary was not compiled to use: AVX2
For maximum performance, you can install NMSLIB from sources
pip install --no-binary :all: nmslib

## Actual code_____no_output_____

class OntoRuler(object):
    def __init__(self):
        self.label = "ontology"
        self.phrase_matcher_attr = "LOWER"
        self.processing_threshold = 100_000
        self.terms = {}
        self.list_of_pattern_dicts = []
#         self.list_of_obj_docs = []
        self.nlp = spacy.load("en_ner_craft_md")
        self.nlp.rename_pipe("ner", "craft_ner")  # To avoid conflict
        # Source for below: https://spacy.io/usage/processing-pipelines
        self.nlp.add_pipe(
            "ner", source=spacy.load("en_core_web_sm"), before="craft_ner"
        )
        self.phrase_matcher = PhraseMatcher(
            self.nlp.vocab, attr=self.phrase_matcher_attr
        )

        with open(os.path.join('input', 'terms.pickle'), "rb") as tf:
            self.terms = pickle.load(tf)
        with open(os.path.join('input', 'patterns.pickle'), "rb") as pf:
            self.list_of_pattern_dicts = pickle.load(pf)

        ruler = self.nlp.add_pipe("entity_ruler", after="craft_ner")
        ruler.add_patterns(self.list_of_pattern_dicts)

        with open(os.path.join('input', 'phrase_matcher.pickle'), "rb") as pmp:
            self.phrase_matcher = pickle.load(pmp)

        # variables for spans and docs extensions
        self.span_term_extension = "is_an_ontology_term"
        self.span_id_extension = "object_id"
        self.has_id_extension = "has_curies"

        Span.set_extension(self.span_term_extension, default=False, force=True)
        Span.set_extension(self.span_id_extension, default=False, force=True)
        Span.set_extension("object_category", default=False, force=True)
        Span.set_extension("object_label", default=False, force=True)
        Span.set_extension("object_match_field", default=False, force=True)
        Span.set_extension("origin", default=False, force=True)
        Span.set_extension("start", default=False, force=True)
        Span.set_extension("end", default=False, force=True)

        Span.set_extension(
            self.has_id_extension, getter=self.has_curies, force=True
        )

        Doc.set_extension(
            self.has_id_extension, getter=self.has_curies, force=True
        )
        Doc.set_extension(self.label.lower(), default=[], force=True)

#         self.nlp.add_pipe(
#             "scispacy_linker",
#             config={"resolve_abbreviations": True, "linker_name": "umls"},
#         )  # Must be one of 'umls' or 'mesh'.

    # getter function for doc level
    def has_curies(self, tokens):
        """Check if any one token has CURIEs

        :param tokens: token
        :return: Boolean
        """
        return any([t._.get(self.span_term_extension) for t in tokens])

    def get_terms_patterns(self, *args):
        """Get dictionaries of terms and patterns,
        along with doc format of the term.

        :return: Dictionary of terms;
        Dictionary of patterns;
        doc object of the term
        """
        origin, object_id, matched_term, description, object_category = args[0]
        terms_dict = {}
        pattern_dict = {}
        object_match_field = ""

        if "[SYNONYM_OF:" in description:
            object_label = description.split("[SYNONYM_OF:")[-1].rstrip("]")
            object_match_field = "hasRelatedSynonym"
        else:
            object_label = matched_term
            # object_match_field = "isExactMatch"

        if matched_term is not None and matched_term == matched_term:
            terms_dict[matched_term.lower()] = {
                "object_id": object_id,
                "object_category": object_category,
                "object_label": object_label,
                "object_match_field": object_match_field,
                "origin": origin,
            }
            pattern_dict["id"] = object_id
            pattern_dict["label"] = origin.split(".")[0] # can also be ID
            pattern_dict["pattern"] = matched_term

        return terms_dict, pattern_dict, self.nlp(matched_term)

    def get_ont_terms_df(self) -> pd.DataFrame:
        """Get Ontology terms from external source in the form of a pandas DataFrame.

        :return: Pandas DataFrame for of termlist.
        """
        cols = [
            "CUI",
            "origin",
            "CURIE",
            "matched_term",
            "description",
            "object_category",
        ]

        ontoTerms = pd.read_csv(os.path.join('terms', 'onto_termlist.tsv'), sep='\t', low_memory=False, names=cols)
        df = ontoTerms.drop_duplicates()
        df.columns = cols
        df = df.drop(["CUI"], axis=1)
        df = df.fillna("")
        return df_____no_output_____%%time
__spec__ = None
onto_ruler_obj = OntoRuler()/opt/anaconda3/envs/ontospacy/lib/python3.9/site-packages/spacy/language.py:710: UserWarning: [W113] Sourced component 'ner' may not work as expected: source vectors are not identical to current pipeline vectors.
  warnings.warn(Warnings.W113.format(name=source_name))
test = """
However, while Dehalococcoides spp.
have received much attention with respect to genome sequencing two published complete genomes,
three draft genomes , only limited efforts have been undertaken to elucidate genomes of
halorespiring bacteria belonging to the low-GC Gram-positive bacteria, which comprise one of
the largest groups of environmental isolates able to grow on the expense of reductive dehalogenation.
In fact, until recently no genome sequencing project has yet been initiated towards Dehalobacter spp.,
an intriguing group of isolates that are restricted in their metabolism to halorespiration, similar
to Dehalococcoides spp. That is why we have now elucidated the genome sequence of Dehalobacter sp.
strain E1, which grows in syntrophic coculture with a fermenative Sedimentibacter sp Maphosa, , unpublished .
In line with the above mentioned lack of information with respect to their genomic microdiversity,
we here propose the genomic sequencing of a number of closely related strains of the
Gram-positive halorespiring phyla Desulfitobacterium and Dehalobacter, which, while highly related
phylogenetically, differ significantly in their biodegradative activities and eco-physiological features,
such as source of isolation, spectrum of electron donors, and alternative electron acceptors.
In addition, we propose to include a number of populations that did not undergo a lengthy
in vitro isolation procedure, which can be expected to impose non-natural selection pressures.
This has become feasible because of recent developments in the areas of whole genome amplication
of small amounts of genomic DNA, in combination with high throughput cultivation of environmental
microorganisms at the microcolony scale , . The recently obtained Fe III -reducing ‚
Deferrispora aromatica gen. nov. sp. nov.‚Äù Kunapuli, , unpublished results ,
phylogenetically most closely related to Desulfitobacterium and Dehalobacter,
is rather dedicated to degradation of a wide range of aromatic hydrocarbons under Fe III -reducing conditions,
and as such the first non-proteobacterial isolate with this feature.
The availability of these genome sequences will provide pivotal information to
further our understanding of evolution, speciation and functionality of this environmentally highly
relevant group of bacteria. The here proposed sequencing project is strongly supported by several
researchers leading in the field of anaerobic degradation of environmental pollutants.
Scientists agreed to actively participate in sequence annotation, building
on their strong expertise from previous and current genome sequence projects.
"""
doc = onto_ruler_obj.nlp(test)
displacy.render(doc, style='ent')_____no_output_____

## Output_____no_output_____

input_text_file = os.path.join('input', 'text.txt')

with open(input_text_file, "r") as txt:
    text = txt.read().replace('\n',"")
    _____no_output_____'''
    PERSON:      People, including fictional.
    NORP:        Nationalities or religious or political groups.
    FAC:         Buildings, airports, highways, bridges, etc.
    ORG:         Companies, agencies, institutions, etc.
    GPE:         Countries, cities, states.
    LOC:         Non-GPE locations, mountain ranges, bodies of water.
    PRODUCT:     Objects, vehicles, foods, etc. (Not services.)
    EVENT:       Named hurricanes, battles, wars, sports events, etc.
    WORK_OF_ART: Titles of books, songs, etc.
    LAW:         Named documents made into laws.
    LANGUAGE:    Any named language.
    DATE:        Absolute or relative dates or periods.
    TIME:        Times smaller than a day.
    PERCENT:     Percentage, including ”%“.
    MONEY:       Monetary values, including unit.
    QUANTITY:    Measurements, as of weight or distance.
    ORDINAL:     “first”, “second”, etc.
    CARDINAL:    Numerals that do not fall under another type.
'''_____no_output_____doc = onto_ruler_obj.nlp(text)
svg = displacy.render(doc, style='ent')
_____no_output_____dframcy = DframCy(onto_ruler_obj.nlp)
annotation_df = dframcy.to_dataframe(doc)_____no_output_____annotation_df.head()_____no_output_____annotation_df[annotation_df["token_ent_type_"] != ""].head()_____no_output_____


# Notebook from lisaplag/pints
Path: examples/toy/model-sir.ipynb

# SIR Epidemiology model: Outbreak on Tristan de Cunha

This example shows how the [SIR model](http://pints.readthedocs.io/en/latest/toy/sir_model.html) can be used.

The SIR model describes a disease breaking out in a population split into a susceptible (S), infected (I), and recovered (R) part (see [wikipedia](https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology#The_SIR_model)).

The formulation shown here was used by [Toni et al.](http://rsif.royalsocietypublishing.org/content/6/31/187.short), in a paper in which they also give real data of a common-cold outbreak on the isle of Tristan de Cunha (originally from [Hammond & Tyrrell](https://www.cambridge.org/core/journals/epidemiology-and-infection/article/mathematical-model-of-commoncold-epidemics-on-tristan-da-cunha/787670BB798E292A9AFE59B6B51DE57A) and [Shibli et al.](https://www.cambridge.org/core/journals/epidemiology-and-infection/article/common-colds-on-tristan-da-cunha/D83E74AB677CCBC74E71A319EAD6C330)). In this formulation, the susceptible population is unknown, the we can observe the infected and recovered population.

The model has three parameters: infection rate ``gamma``, recovery rate ``v``, and the initial susceptible population ``S0``.

_____no_output_____

import pints
import pints.toy
import pints.plot
import numpy as np
import matplotlib.pyplot as plt

# Create a model
model = pints.toy.SIRModel()

# Run a simulation
parameters = model.suggested_parameters()
times = model.suggested_times()
values = model.simulate(parameters, times)

print('Parameters:')
print(parameters)

# Plot the results
plt.figure()
plt.xlabel('Time')
plt.ylabel('Population')
plt.plot(times, values)
plt.legend(['Infected', 'Recovered'])
plt.show()Parameters:
[0.026, 0.285, 38]

We can have a look at the real data too:_____no_output_____

real_values = model.suggested_values()

# Plot the results
plt.figure()
plt.xlabel('Time')
plt.ylabel('Population')
plt.plot(times, real_values)
plt.legend(['Infected', 'Recovered'])
plt.show()_____no_output_____

Next, we set up a problem. Because this model has multiple outputs (2), we use a [MultiOutputProblem](http://pints.readthedocs.io/en/latest/core_classes_and_methods.html#multi-output-problem)._____no_output_____

problem = pints.MultiOutputProblem(model, times, real_values)
score = pints.SumOfSquaresError(problem)_____no_output_____

To create a probabilistic model, we'll assume the data can be perfectly described by the model + Gausian noise with an unknown standard deviation `sigma1` in I and `sigma2` in R._____no_output_____

loglikelihood = pints.GaussianLogLikelihood(problem)_____no_output_____

We can now use MCMC to have a guess at the parameters! We'll try to infer two noise parameters at the same time._____no_output_____

# Starting points
x0 = [
    [0.001, 0.20, 52, 3, 3],
    [0.05, 0.34, 34, 3, 3],
    [0.02, 0.18, 20, 3, 3],
]

# Create MCMC routine
mcmc = pints.MCMCController(loglikelihood, 3, x0)
mcmc.set_max_iterations(3000)
mcmc.set_log_to_screen(False)
chains = mcmc.run()/home/mrobins/git/pints/pints/_log_likelihoods.py:154: RuntimeWarning: invalid value encountered in log
  - np.sum(error**2, axis=0) / (2 * sigma**2))

We now have a look at the rhat and traces, to get an indication of how well the chains have converged:_____no_output_____

# Check convergence using rhat criterion
print('R-hat:')
print(pints.rhat_all_params(chains))

plt.figure()
pints.plot.trace(chains)
plt.show()R-hat:
[1.1404228253914508, 1.1663101654093089, 1.2341689027380545, 1.0649510899918277, 1.116114610283234]

Finally, we compare some predictions based on the final samples to the real data:_____no_output_____

samples = chains[0, -1000:]

plt.figure()
fig, axes = pints.plot.series(samples, problem)
axes[0].plot(times, real_values[:, 0], c='#ff7f0e')
axes[1].plot(times, real_values[:, 1], c='#ff7f0e')
plt.show()_____no_output_____