# -*- coding: utf-8 -*-
#------------------------------------------------------------------
# LEIA E PREENCHA O CABEÇALHO
# NÃO ALTERE OS NOMES DAS FUNÇÕES
# NÃO APAGUE OS DOCSTRINGS
# NÃO INCLUA NENHUM import ...
#------------------------------------------------------------------

'''

    Nome: Rafael Prudêncio Leite
    NUSP: ********

    Ao preencher esse cabeçalho com o meu nome e o meu número USP,
    declaro que todas as partes originais desse exercício programa (EP)
    foram desenvolvidas e implementadas por mim e que portanto não
    constituem desonestidade acadêmica ou plágio.
    Declaro também que sou responsável por todas as cópias desse
    programa e que não distribui ou facilitei a sua distribuição.
    Estou ciente que os casos de plágio e desonestidade acadêmica
    serão tratados segundo os critérios divulgados na página da
    disciplina.
    Entendo que EPs sem assinatura devem receber nota zero e, ainda
    assim, poderão ser punidos por desonestidade acadêmica.

    Abaixo descreva qualquer ajuda que você recebeu para fazer este
    EP.  Inclua qualquer ajuda recebida por pessoas (inclusive
    monitores e colegas). Com exceção de material de MAC0110 e MAC0122,
    caso você tenha utilizado alguma informação, trecho de código,...
    indique esse fato abaixo para que o seu programa não seja
    considerado plágio ou irregular.

    Exemplo:

        A monitora me explicou que eu devia utilizar a função int() quando
        fazemos leitura de números inteiros.

        A minha função quicksort() foi baseada na descrição encontrada na
        página https://www.ime.usp.br/~pf/algoritmos/aulas/quick.html.

    Descrição de ajuda ou indicação de fonte:

'''
import numpy as np

#--------------------------------------------------------------------------
# constantes
BLOCKED = 0  # sítio bloqueado
OPEN    = 1  # sítio aberto
FULL    = 2  # sítio cheio

class Fila:
    def __init__(self):
        self.itens = []

    def vazia(self):
        return self.itens == []

    def insere(self, item):
        self.itens.append(item)

    def remove(self):
        self.itens.pop[0]

class Percolation:
    '''
    Representa uma grade com todos os sítios inicialmente bloqueados.
    '''
    def __init__(self, entrada):
        self.perco = np.full((entrada, entrada), BLOCKED) if type(entrada) == int else np.full(entrada, BLOCKED)
        self.shape = self.perco.shape

    '''
    def shape(self):
        return self.perco.shape
    '''

    def __str__(self):
        n_open = int(0)
        fig = str('\n') + str('+---')*self.perco.shape[1] + '+\n/ '
        for i in range(len(self.perco)):
            for j in range(len(self.perco[0])):
                if self.perco[i, j] == BLOCKED:
                    fig += '  / '
                elif self.perco[i, j] == OPEN:
                    fig += 'o / '
                    n_open += 1
                else:
                    fig += 'x / '
                    n_open += 1
            fig += str('\n') + str('+---')*self.perco.shape[1] + '+\n/ '
        return fig[:-2] + 'grade de dimensão: '+ str(self.perco.shape[0]) + 'x' + str(self.perco.shape[1]) + '\nNúmero de sítios abertos: ' + str(n_open)+'\npercolou: '+ str(perco(self))

    def is_open(self, int1, int2):
        if (int1, int2) >= (0,0) and len(self.perco) - int1 >= 0 and len(self.perco[0]) - int2 >= 0:
            if self.perco[int1, int2] == OPEN or self.perco[int1, int2] == FULL:
                return True
            else:
                return False
        else:
            print('Valor inserido invalido.')
            return None

    def is_full(self, int1, int2):
        if (int1, int2) >= (0,0) and len(self.perco) - int1 >= 0 and len(self.perco[0]) - int2 >= 0:
            if self.perco[int1, int2] == 2:
                return True
            else:
                return False
        else:
            print('Valor inserido invalido.')
            return None

    def no_open(self):
        n_int = 0
        for i in range(0, self.shape[0]):
            for j in range(0, self.shape[1]):
                if self.perco[i,j] == OPEN or self.perco[i, j] == FULL:
                    n_int += 1
        return n_int

    def get_grid(self):
        return np.copy(self.perco)

    def open(self, int1, int2):
        if (int1+1, int2+1) > self.shape or int1<0 or int2<0:
            print('A posição: [{},{}] está fora da grade.'.format(int1,int2))
            return None

        if self.perco[int1, int2] == BLOCKED:
            self.perco[int1, int2] = OPEN
            if int1 == 0:
                self.perco[int1, int2] = FULL
            look(self, [int1, int2])

    def __setitem__(self, instance, value):
        self.perco[instance] = value

    def percolates(self):
        return perco(self)

def perco(matrix):
    for i in range(matrix.shape[1]):
        if matrix.perco[matrix.shape[0]-1, i] == FULL:
            return True
    return False

def look(matrix, pos):
    q = []
    q.append(pos)
    p = []

    while not q == []:

        i = q.pop()[:]
        p.append(i)
        if i[0] + 2 <= matrix.shape[0] and matrix.is_open(i[0]+1, i[1]) == True:
            if matrix.is_full(i[0]+1, i[1]) == True:
                matrix[i[0], i[1]] = FULL
            if [i[0]+1, i[1]] not in p:
                q.append([i[0]+1, i[1]])
                p.append([i[0]+1, i[1]])

        if i[0] > 0 and matrix.is_open(i[0]-1, i[1]) == True:
            if matrix.is_full(i[0]-1, i[1]) == True:
                matrix[i[0], i[1]] = FULL
            if [i[0]-1, i[1]] not in p:
                q.append([i[0]-1, i[1]])
                p.append([i[0]-1, i[1]])

        if i[1] + 2 <= matrix.shape[1] and matrix.is_open(i[0], i[1]+1) == True:
            if matrix.is_full(i[0], i[1]+1) == True:
                matrix[i[0], i[1]] = FULL
            if [i[0], i[1]+1] not in p:
                q.append([i[0], i[1]+1])
                p.append([i[0], i[1]+1])

        if i[1] > 0 and matrix.is_open(i[0], i[1]-1) == True:
            if matrix.is_full(i[0], i[1]-1) == True:
                matrix[i[0], i[1]] = FULL
            if [i[0], i[1]-1] not in p:
                q.append([i[0], i[1]-1])
                p.append([i[0], i[1]-1])

'''
def look(matrix, pos): #array, tuple
    if matrix.perco[pos[0], pos[1]] == OPEN:

        if pos[0] == 0:
            matrix[pos[0], pos[1]] = FULL

        if pos[0] > 0: #olha de baixo p/ cima
            if matrix.is_open(pos[0]-1, pos[1]) == True:
                if matrix.is_full(pos[0]-1, pos[1]) == True:
                    matrix[pos[0], pos[1]] = FULL
                look(matrix, (pos[0]-1, pos[1]))

        if pos[1] + 2 <= matrix.shape[1]: #olha da esq p/ dir
            if matrix.is_open(pos[0], pos[1]+1) == True:
                if matrix.is_full(pos[0], pos[1]+1) == True:
                    matrix[pos[0], pos[1]] = FULL #TEM UM ELSE: AQUI EM BAIXO???
                look(matrix, (pos[0], pos[1]+1))

        if pos[1] > 0: #olha da dir p/ esq
            if matrix.is_open(pos[0], pos[1]-1) == True:
                if matrix.is_full(pos[0], pos[1]-1) == True:
                    matrix[pos[0], pos[1]] = FULL
                look(matrix, (pos[0], pos[1]-1))
'''


"""
DROP: A Reading Comprehension Benchmark Requiring Discrete Reasoning Over Paragraphs
https://aclanthology.org/attachments/N19-1246.Supplementary.pdf

DROP is a QA dataset which tests comprehensive understanding of paragraphs. In
this crowdsourced, adversarially-created, 96k question-answering benchmark, a
system must resolve multiple references in a question, map them onto a paragraph,
and perform discrete operations over them (such as addition, counting, or sorting).

Homepage: https://allenai.org/data/drop

Acknowledgement: This implementation is based on the official evaluation for `DROP`:
https://github.com/allenai/allennlp-reading-comprehension/blob/master/allennlp_rc/eval/drop_eval.py
"""
import inspect
import numpy as np
import re
import string
import lm_eval.datasets.drop.drop
from scipy.optimize import linear_sum_assignment
from lm_eval.base import Task, rf
from lm_eval.metrics import mean

_CITATION = """
@misc{dua2019drop,
    title={DROP: A Reading Comprehension Benchmark Requiring Discrete Reasoning Over Paragraphs},
    author={Dheeru Dua and Yizhong Wang and Pradeep Dasigi and Gabriel Stanovsky and Sameer Singh and Matt Gardner},
    year={2019},
    eprint={1903.00161},
    archivePrefix={arXiv},
    primaryClass={cs.CL}
}
"""

_ARTICLES = re.compile(r"\b(a|an|the)\b", re.UNICODE)

class DROP(Task):
    VERSION = 1
    DATASET_PATH = inspect.getfile(lm_eval.datasets.drop.drop)
    DATASET_NAME = None

    def has_training_docs(self):
        return True

    def has_validation_docs(self):
        return True

    def has_test_docs(self):
        return False

    def training_docs(self):
        if self._training_docs is None:
            self._training_docs = list(map(self._process_doc, self.dataset["train"]))
        return self._training_docs

    def validation_docs(self):
        return map(self._process_doc, self.dataset["validation"])

    def _process_doc(self, doc):
        return {
            "id": doc["query_id"],
            "passage": doc["passage"],
            "question": doc["question"],
            "answers": self.get_answers(doc),
        }

    @classmethod
    def get_answers(cls, qa):
        def _flatten_validated_answers(validated_answers):
            """Flattens a dict of lists of validated answers.
            {"number": ['1', '8'], ...}
            -> [{"number": ['1'], ...}, {"number": ['8'], ...}]
            """
            valid_answers = []
            for i in range(len(validated_answers["number"])):
                valid_answers.append(
                    {
                        "number": validated_answers["number"][i],
                        "date": validated_answers["date"][i],
                        "spans": validated_answers["spans"][i],
                    }
                )
            return valid_answers

        answers = []
        answers_set = set()
        candidates = [qa["answer"]] + _flatten_validated_answers(
            qa["validated_answers"]
        )
        for candidate in candidates:
            answer = cls.parse_answer(candidate)
            if answer in answers_set:
                continue
            answers_set.add(answer)
            answers.append(answer)
        return answers

    @classmethod
    def parse_answer(cls, answer):
        # NOTE: Everything is returned as a tuple for uniformity and hashability.
        if answer["number"] != "":
            return (str(answer["number"]),)
        if answer["spans"] != []:
            return tuple(answer["spans"])
        return (
            " ".join(
                [answer["date"]["day"], answer["date"]["month"], answer["date"]["year"]]
            ).strip(),
        )

    def doc_to_text(self, doc):
        return f"Passage: {doc['passage']}\nQuestion: {doc['question']}\nAnswer:"

    def should_decontaminate(self):
        return True

    def doc_to_decontamination_query(self, doc):
        return doc["passage"] + " " + doc["question"]

    def doc_to_target(self, doc):
        return " " + ", ".join(doc["answers"][0])

    def construct_requests(self, doc, ctx):
        """Uses RequestFactory to construct Requests and returns an iterable of
        Requests which will be sent to the LM.

        :param doc:
            The document as returned from training_docs, validation_docs, or test_docs.
        :param ctx: str
            The context string, generated by fewshot_context. This includes the natural
            language description, as well as the few shot examples, and the question
            part of the document for `doc`.
        """
        conts = [rf.greedy_until(ctx, ["."])]
        return conts

    def process_results(self, doc, results):
        """Take a single document and the LM results and evaluates, returning a
        dict where keys are the names of submetrics and values are the values of
        the metric for that one document

        :param doc:
            The document as returned from training_docs, validation_docs, or test_docs.
        :param results:
            The results of the requests created in construct_requests.
        """
        preds, golds = results, doc["answers"]
        max_em = 0
        max_f1 = 0
        for gold_answer in golds:
            exact_match, f1_score = self.get_metrics(preds, gold_answer)
            if gold_answer[0].strip():
                max_em = max(max_em, exact_match)
                max_f1 = max(max_f1, f1_score)
        return {"em": max_em, "f1": max_f1}

    def get_metrics(self, predicted, gold):
        """
        Takes a predicted answer and a gold answer (that are both either a string or a list of
        strings), and returns exact match and the DROP F1 metric for the prediction.  If you are
        writing a script for evaluating objects in memory (say, the output of predictions during
        validation, or while training), this is the function you want to call, after using
        :func:`answer_json_to_strings` when reading the gold answer from the released data file.
        """
        predicted_bags = self._answer_to_bags(predicted)
        gold_bags = self._answer_to_bags(gold)

        if set(predicted_bags[0]) == set(gold_bags[0]) and len(
            predicted_bags[0]
        ) == len(gold_bags[0]):
            exact_match = 1.0
        else:
            exact_match = 0.0

        f1_per_bag = self._align_bags(predicted_bags[1], gold_bags[1])
        f1 = np.mean(f1_per_bag)
        f1 = round(f1, 2)
        return exact_match, f1

    def _answer_to_bags(self, answer):
        if isinstance(answer, (list, tuple)):
            raw_spans = answer
        else:
            raw_spans = [answer]
        normalized_spans = []
        token_bags = []
        for raw_span in raw_spans:
            normalized_span = self._normalize(raw_span)
            normalized_spans.append(normalized_span)
            token_bags.append(set(normalized_span.split()))
        return normalized_spans, token_bags

    def _align_bags(self, predicted, gold):
        """
        Takes gold and predicted answer sets and first finds the optimal 1-1 alignment
        between them and gets maximum metric values over all the answers.
        """
        scores = np.zeros([len(gold), len(predicted)])
        for gold_index, gold_item in enumerate(gold):
            for pred_index, pred_item in enumerate(predicted):
                if self._match_numbers_if_present(gold_item, pred_item):
                    scores[gold_index, pred_index] = self._compute_f1(
                        pred_item, gold_item
                    )
        row_ind, col_ind = linear_sum_assignment(-scores)

        max_scores = np.zeros([max(len(gold), len(predicted))])
        for row, column in zip(row_ind, col_ind):
            max_scores[row] = max(max_scores[row], scores[row, column])
        return max_scores

    def _compute_f1(self, predicted_bag, gold_bag):
        intersection = len(gold_bag.intersection(predicted_bag))
        if not predicted_bag:
            precision = 1.0
        else:
            precision = intersection / float(len(predicted_bag))
        if not gold_bag:
            recall = 1.0
        else:
            recall = intersection / float(len(gold_bag))
        f1 = (
            (2 * precision * recall) / (precision + recall)
            if not (precision == 0.0 and recall == 0.0)
            else 0.0
        )
        return f1

    def _match_numbers_if_present(self, gold_bag, predicted_bag):
        gold_numbers = set()
        predicted_numbers = set()
        for word in gold_bag:
            if self._is_number(word):
                gold_numbers.add(word)
        for word in predicted_bag:
            if self._is_number(word):
                predicted_numbers.add(word)
        if (not gold_numbers) or gold_numbers.intersection(predicted_numbers):
            return True
        return False

    def _is_number(self, text):
        try:
            float(text)
            return True
        except ValueError:
            return False

    def _remove_articles(self, text):
        return _ARTICLES.sub(" ", text)

    def _white_space_fix(self, text):
        return " ".join(text.split())

    def _remove_punc(self, text):
        exclude = set(string.punctuation)
        if not self._is_number(text):
            return "".join(ch for ch in text if ch not in exclude)
        else:
            return text

    def _fix_number(self, text):
        return str(float(text)) if self._is_number(text) else text

    def _tokenize(self, text):
        return re.split(" |-", text)

    def _normalize(self, answer):
        tokens = [
            self._white_space_fix(
                self._remove_articles(
                    self._fix_number(self._remove_punc(token.lower()))
                )
            )
            for token in self._tokenize(answer)
        ]
        tokens = [token for token in tokens if token.strip()]
        normalized = " ".join(tokens).strip()
        return normalized

    def aggregation(self):
        """
        :returns: {str: [float] -> float}
            A dictionary where keys are the names of submetrics and values are
            functions that aggregate a list of metrics
        """
        return {"em": mean, "f1": mean}

    def higher_is_better(self):
        """
        :returns: {str: bool}
            A dictionary where keys are the names of submetrics and values are
            whether a higher value of the submetric is better
        """
        return {"em": True, "f1": True}


![Callysto.ca Banner](https://github.com/callysto/curriculum-notebooks/blob/master/callysto-notebook-banner-top.jpg?raw=true)

%%html

<form action="javascript:code_toggle()"><input type="submit" id="toggleButton" value="Show Code"></form>

from ipywidgets import Output, IntSlider, VBox, Layout
from IPython.display import Javascript,clear_output, display, HTML
import ipywidgets as widgets
import random
from plotly.offline import init_notebook_mode, iplot
from ipywidgets import HBox
import plotly.graph_objs as go
import numpy as np
import random

init_notebook_mode(connected=True)

#This function produces a multiple choice form with four options
def multiple_choice(option_1, option_2, option_3, option_4):
    option_list = [option_1, option_2, option_3, option_4]
    answer = option_list[0]
    letters = ["(A) ", "(B) ", "(C) ", "(D) "]

    #Boldface letters at the beginning of each option
    start_bold = "\033[1m"; end_bold = "\033[0;0m"

    #Randomly shuffle the options
    random.shuffle(option_list)

    #Prints the letters (A) to (D) in sequence with randomly chosen options
    for i in range(4):
        option_text = option_list.pop()
        print(start_bold + letters[i] + end_bold + option_text)

        #Stores the correct answer
        if option_text == answer:
            letter_answer = letters[i]

    button1 = widgets.Button(description="(A)"); button2 = widgets.Button(description="(B)")
    button3 = widgets.Button(description="(C)"); button4 = widgets.Button(description="(D)")

    button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Whitesmoke'
    button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Whitesmoke'

    container = widgets.HBox(children=[button1,button2,button3,button4])
    display(container)
    print(" ", end='\r')

    def on_button1_clicked(b):
        if "(A) " == letter_answer:
            print("Correct! 👏", end='\r')
            button1.style.button_color = '#abffa8'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Whitesmoke'
        else:
            print("Try again! ", end='\r')
            button1.style.button_color = '#ffbbb8'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Whitesmoke'

    def on_button2_clicked(b):
        if "(B) " == letter_answer:
            print("Correct! 👏", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = '#abffa8'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Whitesmoke'
        else:
            print("Try again! ", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = '#ffbbb8'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = 'Whitesmoke'

    def on_button3_clicked(b):
        if "(C) " == letter_answer:
            print("Correct! 👏", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = '#abffa8'; button4.style.button_color = 'Whitesmoke'
        else:
            print("Try again! ", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = '#ffbbb8'; button4.style.button_color = 'Whitesmoke'

    def on_button4_clicked(b):
        if "(D) " == letter_answer:
            print("Correct! 👏", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = '#abffa8'
        else:
            print("Try again! ", end='\r')
            button1.style.button_color = 'Whitesmoke'; button2.style.button_color = 'Whitesmoke'
            button3.style.button_color = 'Whitesmoke'; button4.style.button_color = '#ffbbb8'

    button1.on_click(on_button1_clicked); button2.on_click(on_button2_clicked)
    button3.on_click(on_button3_clicked); button4.on_click(on_button4_clicked)

def run_all(ev):
    display(Javascript('IPython.notebook.execute_cell()'))

# Thermal Linear Expansion

## Introduction

The goal of this notebook is to discuss the following:
- Review **Kinetic Molecular Theory** and how it relates to thermal expansion.
- Derive a **formula** to compute thermal expansion.
- **Apply** this formula to real-world scenarios.

## Kinetic Molecular Theory

**Summary:**

- Matter is made up of particles that are constantly moving.
- The temperature of a substance is a measure of the average kinetic energy of the particles.
- There are spaces between particles of matter. The average amount of empty space between molecules gets progressively larger as a sample of matter moves from solid to the liquid and gas phases.

start_bold = "\033[1m"; end_bold = "\033[0;0m"
question = start_bold + "Key Question: " + end_bold + \
"Most solids " + start_bold + "expand" + end_bold + " when heated. Based on the points above, " + start_bold + "why is this the case?" + end_bold
print(question)
print("")
option_1 = "When heat is added to a substance, the molecules vibrate faster causing the space between molecules to increase."
option_2 = "An increase in temperature causes the molecules to loosen up and move freely but slowly around."
option_3 = "As objects become hotter, they contract and the molecules increase their speed."
option_4 = "An increase in temperature causes the molecules to tighten up and move in a constricted manner."

multiple_choice(option_1, option_2, option_3, option_4)

## Thermal Linear Expansion

**Definition:** The increase in length of a solid in one direction (due to an increase in temperature) is called *thermal linear expansion*.

**Goal:** Given any solid, we wish to **predict** how much the solid will expand with a change in temperature; i.e. how do we predict the *change in length* ($\Delta L$) of a solid due to an increase in temperature?

start_bold = "\033[1m"; end_bold = "\033[0;0m"
question = start_bold + "Key Question: " + end_bold + "What " + start_bold + "variable(s)" + end_bold + " would you need know to predict how much a substance will expand when heat is added?"
print(question)
print("")
option_1 = "Type of material, Initial length/size, Amount of heat added"
option_2 = "Type of material, Amount of heat added"
option_3 = "Pressure acting material, Amount of heat added"
option_4 = "Roughness of the material, Type of material"

multiple_choice(option_1, option_2, option_3, option_4)

**Fact:** The change of length $(\Delta L)$ of a solid depends on **three** factors:
1. *Initial length* of the solid $(L)$
2. *Temperature change* $(\Delta T)$
3. Type of material, distinguished by a constant value named the *coefficient of linear expansion* $(\alpha)$

Some values for the coefficient of linear expansion are shown in the table below:

 Materials                 | Coefficient of linear expansion, $\alpha$ $\:$ ($1/°\rm{C}$)
 ---                       | ---
 Aluminum                  | 25 $\times$ 10$^{-6}$
 Concrete, iron, steel     | 12 $\times$ 10$^{-6}$
 Copper                    | 16 $\times$ 10$^{-6}$
 Glass (soft), Platinum    | 9 $\times$ 10$^{-6}$
 Glass (Pyrex)             | 3 $\times$ 10$^{-6}$

The following formula relates these **four quantities variables** together:

$$\Delta L = \alpha L \Delta T$$

We will derive this formula later on in the notebook. Before that, let's get accustomed to using this formula in practical situations.

### Example

The longest continuous bridge in Saskatchewan is a 380 m long steel bridge in North Battleford. The temperature in the area varies from -40.0 $^\circ$C to 30.0 $^\circ$C. What is the *change in length of the bridge*? Round to two significant figures.

out1 = Output()
Step1B = widgets.Button(description="STEP 1", layout=Layout(width='20%', height='100%'))
count1 = 1

info1 = widgets.HTMLMath(value="Identify the variables that we know and what we're looking for:")
math_text1 = widgets.HTMLMath(value="Initial length: $L=380\:m$")
math_text2 = widgets.HTMLMath(value="Change in temperature: $\Delta T = T_{final} - T_{initial}$ = 30°C $-$ ($-$40°C) = 70°C")
math_text3 = widgets.HTMLMath(value="Coefficient of linear expansion (specific to steel): " + chr(0x3B1) + " = $12 × 10^{-6}$ °C$^{-1}$ (reference from table)")
math_text4 = widgets.HTMLMath(value="Change in length of the bridge:  $\Delta L$ = ?")

def on_Step1B_clicked(b):
    global count1
    count1 += 1
    with out1:
        clear_output()
        if count1 % 2 == 0:
            display(info1, math_text1, math_text2, math_text3, math_text4)

display(VBox([Step1B, out1]))
Step1B.on_click(on_Step1B_clicked)

out2 = Output()
Step2B = widgets.Button(description="STEP 2", layout=Layout(width='20%', height='100%'))
count2 = 1

info2 = widgets.HTMLMath(value="Substitute each known value into the formula and solve for the missing variable:")
math_text5 = widgets.HTMLMath(value="$\Delta L$ = " + chr(0x3B1) + " L $\Delta T$ = ($12 × 10^{-6}$ °C$^{-1}$)(380 m)(70 °C) = 0.3192 m")

def on_Step2B_clicked(b):
    global count2
    count2 += 1
    with out2:
        clear_output()
        if count2 % 2 == 0:
            display(info2, math_text5)

display(VBox([Step2B, out2]))
Step2B.on_click(on_Step2B_clicked)

out3 = Output()
Step3B = widgets.Button(description="STEP 3", layout=Layout(width='20%', height='100%'))
count3 = 1

info3 = widgets.HTMLMath(value="Round to the correct number of significant figures and convert to the correct units (if needed):")
math_text6 = widgets.HTMLMath(value="0.3192 m = 0.32 m or 32 cm")

def on_Step3B_clicked(b):
    global count3
    count3 += 1
    with out3:
        clear_output()
        if count3 % 2 == 0:
            display(info3, math_text6)

display(VBox([Step3B, out3]))
Step3B.on_click(on_Step3B_clicked)

### Practice
Try multiple practice problems by clicking the 'Generate New Question' button upon completing a problem.

button = widgets.Button(description="Generate New Question", layout=Layout(width='20%', height='100%'))
button.on_click(run_all)
display(button)

#Variables to randomize
initial_length = round(random.uniform(2.0, 6.0), 2)
initial_temp = round(random.uniform(-50.0, -20.0), 1)
final_temp = round(random.uniform(30.0, 45.0), 1)

#Dictionary of different materials
materials = {"aluminum": 0.000025, "iron": 0.000012, "steel": 0.000012, "glass (soft)": 0.000009, "glass (pyrex)": 0.000003, "concrete": 0.000012, "platinum": 0.000009, "copper": 0.000016}
chosen_material = random.choice(list(materials.keys()))

#Print question
question = "A piece of {} is {} m on a cold winter day ({} °C). How much longer is it on a very hot summer day ({} °C)? Round to two decimal places.".format(chosen_material, initial_length, initial_temp, final_temp)
print(question)

#Answer calculation
answer = round((initial_length * materials[chosen_material] * (final_temp - initial_temp))*100, 2)

#Define range of values for random multiple choices
mini = 1
maxa = 100

#Create three choices that are unique (and not equal to the answer)
choice_list = random.sample(range(mini,maxa),3)
while choice_list.count(int(answer*100)) >= 1:
    choice_list = random.sample(range(mini,maxa),3)

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = '{:.2f}'.format(answer) + " cm"
option_2 = '{:.2f}'.format(choice_list[0]/100) + " cm"
option_3 = '{:.2f}'.format(choice_list[1]/100) + " cm"
option_4 = '{:.2f}'.format(choice_list[2]/100) + " cm"

multiple_choice(option_1, option_2, option_3, option_4)

## Deriving the Formula

Now that you are comfortable using the thermal expansion formula, we now discuss how it is derived experimentally. Of the four quantities in the equation, $\Delta L = \alpha L \Delta T$, it is the coefficient of linear expansion ($\alpha$) that may be still be mysterious in how it is measured. Previously, we have referenced a table of values to determine $\alpha$, but *where do these values come from?*

We demonstrate how to calculate $\alpha$ experimentally below. We will require two different materials so we can compare each expansion (which you can select in the dropdown menus below). We then apply *the exact same amount of heat to both materials* (which you can adjust using the slider). When you are ready, click the "Calculate Linear Expansion" button below.

coefficients = {"Aluminum": 0.000025, "Iron": 0.000012, "Steel": 0.000012, "Glass (soft)": 0.000009, "Glass (Pyrex)": 0.000003, "Concrete": 0.000012, "Platinum": 0.000009, "Copper": 0.000016}

graph_out = Output()

selection1 = widgets.Dropdown(
    options={'Aluminum', 'Steel', 'Copper'},
    value='Aluminum',
    description='Material 1:',
)

selection2 = widgets.Dropdown(
    options={'Iron', 'Platinum', 'Glass (Pyrex)'},
    value='Iron',
    description='Material 2:',
)

temp_slider = IntSlider(continuous_update=False, wait=True, value=500, min=50, max=1000, step=50, description=chr(0x0394) + 'T (°C)')
initial_length_slider = IntSlider(continuous_update=False, wait=True, value=5, min=1, max=10, step=1, description='L (m)')

submit_button = widgets.Button(description="Calculate Linear Expansion", layout=Layout(width='20%', height='88%'),button_style='success')

################ Functions ##################

def initial_graph(material1, material2):
################# Parametric Equations #################
    s3 = np.linspace(0, 2 * np.pi, 30)
    t3 = np.linspace(0, 5, 30)
    tGrid3, sGrid3 = np.meshgrid(s3, t3)

    x3 = np.cos(tGrid3) + 1.5
    y3 = np.sin(tGrid3) + 1.5
    z3 = sGrid3

    s4 = np.linspace(0, 2 * np.pi, 30)
    t4 = np.linspace(0, 5, 30)
    tGrid4, sGrid4 = np.meshgrid(s4, t4)

    x4 = np.cos(tGrid4) + 3.5
    y4 = np.sin(tGrid4) + 3.5
    z4 = sGrid4

    ######################################################

    trace1 = go.Surface(x=x3, y=y3, z=z3, colorscale='Greens', showscale=False, text = material1, hoverinfo='text')
    trace2 = go.Surface(x=x4, y=y4, z=z4, colorscale='Blues', showscale=False, text = material2, hoverinfo='text')
    data = [trace1, trace2]
    layout = go.Layout(title = material1 + " and " + material2 + " (before heat is applied)",
    scene = dict(xaxis = dict(title='', range = [0,5],
                     backgroundcolor="rgb(200, 200, 230)",
                     gridcolor="rgb(255, 255, 255)",
                     showbackground=True,
                     zerolinecolor="rgb(255, 255, 255)",
                     showticklabels=False,),
                yaxis = dict(title='', range = [0,5],
                    backgroundcolor="rgb(230, 200,230)",
                    gridcolor="rgb(255, 255, 255)",
                    showbackground=True,
                    zerolinecolor="rgb(255, 255, 255)",
                    showticklabels=False),
                zaxis = dict(title='', range = [0,4],
                    backgroundcolor="rgb(230, 230,200)",
                    gridcolor="rgb(255, 255, 255)",
                    showbackground=True,
                    zerolinecolor="rgb(255, 255, 255)",
                    showticklabels=False,),),
              )
    config={'showLink': False, 'editable': False}
    fig = go.Figure(data=data, layout=layout)
    iplot(fig, config=config)

def graph(h, h2, material1, material2, initial_length):
    with graph_out:
        clear_output(wait=True)

        ################# Parametric Equations #################
        s = np.linspace(0, 2 * np.pi, 30)
        t = np.linspace(0, h, 30)
        tGrid, sGrid = np.meshgrid(s, t)

        x = np.cos(tGrid) + 1.5
        y = np.sin(tGrid) + 1.5
        z = sGrid + initial_length

        s2 = np.linspace(0, 2 * np.pi, 30)
        t2 = np.linspace(0, h2, 30)
        tGrid2, sGrid2 = np.meshgrid(s2, t2)

        x2 = np.cos(tGrid2) + 3.5
        y2 = np.sin(tGrid2) + 3.5
        z2 = sGrid2 + initial_length

        s3 = np.linspace(0, 2 * np.pi, 30)
        t3 = np.linspace(0, initial_length, 30)
        tGrid3, sGrid3 = np.meshgrid(s3, t3)

        x3 = np.cos(tGrid3) + 1.5
        y3 = np.sin(tGrid3) + 1.5
        z3 = sGrid3

        s4 = np.linspace(0, 2 * np.pi, 30)
        t4 = np.linspace(0, initial_length, 30)
        tGrid4, sGrid4 = np.meshgrid(s4, t4)

        x4 = np.cos(tGrid4) + 3.5
        y4 = np.sin(tGrid4) + 3.5
        z4 = sGrid4
        ######################################################

        trace1 = go.Surface(x=x, y=y, z=z, colorscale='Reds', showscale=False, text = chr(0x0394) + 'L (' + material1 + ') = ' + str(round(h/5 * 100 ,5)) + ' cm', hoverinfo='text')
        trace2 = go.Surface(x=x2, y=y2, z=z2, colorscale='Reds', showscale=False, text = chr(0x0394) + 'L (' + material2 + ') = ' + str(round(h2/5 * 100,5)) + ' cm', hoverinfo='text')
        trace3 = go.Surface(x=x3, y=y3, z=z3, colorscale='Greens', showscale=False, text = material1, hoverinfo='text')
        trace4 = go.Surface(x=x4, y=y4, z=z4, colorscale='Blues', showscale=False, text = material2, hoverinfo='text')

        data = [trace1, trace2, trace3, trace4]
        layout = go.Layout(title = chr(0x0394) + 'L (' + material1 + ') = ' + str(round(h/5 * 100 ,5)) + ' cm;  ' + chr(0x0394) + 'L (' + material2 + ') = ' + str(round(h2/5 * 100,5)) + ' cm',
                    scene = dict(
                    xaxis = dict(title='', range = [0,5],
                         backgroundcolor="rgb(200, 200, 230)",
                         gridcolor="rgb(255, 255, 255)",
                         showbackground=True,
                         zerolinecolor="rgb(255, 255, 255)",
                         showticklabels=False,),
                    yaxis = dict(title='', range = [0,5],
                        backgroundcolor="rgb(230, 200,230)",
                        gridcolor="rgb(255, 255, 255)",
                        showbackground=True,
                        zerolinecolor="rgb(255, 255, 255)",
                        showticklabels=False),
                    zaxis = dict(title='', range = [0,initial_length + max(h,h2)],
                        backgroundcolor="rgb(230, 230,200)",
                        gridcolor="rgb(255, 255, 255)",
                        showbackground=True,
                        zerolinecolor="rgb(255, 255, 255)",
                        showticklabels=False,),),
                  )
        config={'showLink': False, 'editable': False}
        fig = go.Figure(data=data, layout=layout)
        note = widgets.HTMLMath(value=r"$\textbf{Note: } \text{The thermal expansion shown below (coloured red) is magnified (5x) for easier viewing.}$")
        display(note)
        iplot(fig, config=config)

def on_submit_button_clicked(b):
    height = (initial_length_slider.value*temp_slider.value*coefficients[selection1.value]) * 5
    height2 = (initial_length_slider.value*temp_slider.value*coefficients[selection2.value]) *  5
    graph(height, height2, selection1.value, selection2.value, initial_length_slider.value)

display(HBox([VBox([selection1, selection2]), VBox([temp_slider, initial_length_slider])]))
display(submit_button)
display(graph_out)

with graph_out:
    initial_graph(selection1.value, selection2.value)

submit_button.on_click(on_submit_button_clicked)

*Use the interactive simulation above to answer the following two questions.*

start_bold = "\033[1m"; end_bold = "\033[0;0m"
question = "Keep ΔT fixed and only adjust L. Run the simulation many times, each with a different value for L (but the same value for ΔT). What happens to ΔL?"
print(question)
print("")
option_1 = "ΔL increases as L increases and ΔL decreases as L decreases."
option_2 = "ΔL increases as L decreases and ΔL decreases as L increases."
option_3 = "There is no relationship between ΔL and L."
option_4 = "When L decreases or increases, ΔL remains constant."

multiple_choice(option_1, option_2, option_3, option_4)

start_bold = "\033[1m"; end_bold = "\033[0;0m"
question = "Now, keep L fixed and only adjust ΔT. Again, run the simulation many times, each with a different value for ΔT (but the same value for L). What happens to ΔL?"
print(question)
print("")
option_1 = "ΔL increases as ΔT increases and ΔL decreases as ΔT decreases."
option_2 = "ΔL increases as ΔT decreases and ΔL decreases as ΔT increases."
option_3 = "There is no relationship between ΔL and ΔT."
option_4 = "When ΔT decreases or increases, ΔL remains constant."

multiple_choice(option_1, option_2, option_3, option_4)

**Goal:** To establish a thermal expansion formula capable of accurately predicting how much a material will expand linearly when heat is applied.

Observe how $\Delta L$ is different for some materials (aluminum versus platinum), but the same for others (steel versus iron). This is due to similarities and differences between the chemical composition of each material. For example, steel is comprised of around 90-95% iron, which accounts for both materials' identical rate of thermal expansion.

Simply knowing the initial length ($L$) and change in temperature ($\Delta T$) is clearly not enough to predict change in length ($\Delta L$). We can see this above: when both materials share the same values of $L$ and $\Delta T$, their $\Delta L$ is often different. We somehow must assign a *constant* value (which we call $\alpha$) for each material that we can use alongside $L$ and $\Delta T$ to compute $\Delta L$. Since we experimentally measured $\Delta L$ above, we in fact have all the information we need to establish such a constant.

The above simulation and questions indicates that the relationship between $L$, $\Delta T$, and $\Delta L$ is direct rather than inverse ($1/\Delta T$ or $1/L$). Since $\Delta L$ becomes larger as both $L$ and $\Delta T$ become larger, we guess that their relationship is multiplicative. Hence, we begin by writing down the following formula that we want:

$$\Delta L = \alpha L \Delta T$$

The $\alpha$ is the constant that we need, but do not know. However, we measured $\Delta L$ and we know both $L$ and $\Delta T$, so computing $\alpha$ is easy:

$$\alpha  = \dfrac{\Delta L}{L \Delta T}$$

Using the sliders below, input the change in length ($\Delta L$) for both materials 1 and 2 from the above experimental simulation to compute $\alpha$ for each material. **Hint:** Use your *arrow keys* on your keyboard to use the sliders with precision.

out_text = Output()

################ Widgets #################
style = {'description_width': 'initial'}
temp1_slider = IntSlider(continuous_update=False, wait=True, value=500, min=10, max=1000, step=1, description=chr(0x0394) + 'T (°C)')
change1_slider = widgets.FloatSlider(continuous_update=False, wait=True, value=0, min=0, max=10, step=0.001, readout_format='.3f', description=chr(0x0394) + 'L'+ chr(0x2081) + ' (cm)', style=style)
change2_slider = widgets.FloatSlider(continuous_update=False, wait=True, value=0, min=0, max=10, step=0.001, readout_format='.3f', description=chr(0x0394) + 'L'+ chr(0x2082) + ' (cm)', style=style)
coeff_calc = widgets.Button(description="Calculate " + chr(0x03b1) + chr(0x2081) + " and " + chr(0x03b1) + chr(0x2082), layout=Layout(width='20%', height='88%'),button_style='success')
###########################################

display(VBox([temp1_slider, change1_slider, change2_slider, coeff_calc]))

def on_submit_button_clicked(b):
    ans1 = round(change1_slider.value / (300.0 * temp1_slider.value), 8)
    ans2 = round(change2_slider.value / (300.0 * temp1_slider.value), 8)
    with out_text:
        clear_output()
        text_answer = widgets.HTMLMath(value=r"$\alpha_1 = $ " + str(ans1) + " °C$^{-1}$" + "$\qquad$" + r"$\alpha_2 = $ " + str(ans2) + " °C$^{-1}$")
        text_answer2 = widgets.HTMLMath(value=r"Compare each $\alpha_1$ and $\alpha_2$ to those found in the table of coefficients of linear expansion above. See if they match.")
        display(text_answer)
        display(text_answer2)

### Widget Interaction Function Calls ###
coeff_calc.on_click(on_submit_button_clicked)

display(out_text)

## Variations on the Thermal Expansion Formula

As we've just seen, the formula $\Delta L = \alpha L \Delta T$ is not always used to find $\Delta L$. Often, we can experimentally measure $\Delta L$. Depending on the application, we can then theoretically compute one of the other quantities (assuming we know the value of two of the other quantities). It is worth noting the three variations of the thermal expansion formula:

$$\alpha = \dfrac{\Delta L}{L \Delta T} \qquad L = \dfrac{\Delta L}{\alpha \Delta T} \qquad \Delta T = \dfrac{\Delta L}{\alpha L}$$

**Note:** This is **one** formula being displayed in **three** different ways. It is convenient to make use of the variation that corresponds to the unknown value that you wish to determine.

### Practice

Try the three different types of questions using the above variations of the thermal expansion formula. Use the 'Generate New Question' button to complete additional practice problems.

button1 = widgets.Button(description="Generate New Question", layout=Layout(width='20%', height='100%'))
button1.on_click(run_all)
display(button1)

#Variables to randomize
change_length = round(random.uniform(0.2, 0.9), 2)
initial_length = round(random.uniform(1.0, 3.0), 2)
initial_temp = round(random.uniform(10.0, 25.0), 1)
final_temp = round(random.uniform(100.0, 150.0), 1)

#Print question
question = "An newly made synthetic material {} m long expands {} mm when heated from {}°C to {}°C. What is the coefficient of linear expansion of this new material?".format(initial_length, change_length, initial_temp, final_temp)
print(question)

#Answer calculation
answer = round((change_length/1000) / (initial_length * (final_temp - initial_temp)), 8)

#Define range of values for random multiple choices
mini = 100
maxa = 900

#Create three choices that are unique (and not equal to the answer)
choice_list = random.sample(range(mini,maxa),3)
while choice_list.count(int(answer*100000000)) >= 1:
    choice_list = random.sample(range(mini,maxa),3)

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = str(answer) + " °C" + chr(0x207B) + chr(0x00B9)
option_2 = str(choice_list[0]/100000000) + " °C" + chr(0x207B) + chr(0x00B9)
option_3 = str(choice_list[1]/100000000) + " °C" + chr(0x207B) + chr(0x00B9)
option_4 = str(choice_list[2]/100000000) + " °C" + chr(0x207B) + chr(0x00B9)

multiple_choice(option_1, option_2, option_3, option_4)

button2 = widgets.Button(description="Generate New Question", layout=Layout(width='20%', height='100%'))
button2.on_click(run_all)
display(button2)

#Variables to randomize
change_length = round(random.uniform(0.5, 2.0), 2)
initial_temp = round(random.uniform(20.0, 30.0), 1)
final_temp = round(random.uniform(100.0, 250.0), 1)

#Dictionary of different materials
materials = {"aluminum": 0.000025, "iron": 0.000012, "steel": 0.000012, "glass (soft)": 0.000009, "glass (pyrex)": 0.000003, "concrete": 0.000012, "platinum": 0.000009, "copper": 0.000016}
chosen_material = random.choice(list(materials.keys()))

#Print question
question = "A piece of {} changes in length by {} m. The initial temperature was {}°C and the final temperature was {}°C. Determine the original length of the material. Leave your answer unrounded.".format(chosen_material, change_length, initial_temp, final_temp)
print(question)

#Answer calculation
answer = round(change_length / (materials[chosen_material] * (final_temp - initial_temp)), 2)

#Define range of values for random multiple choices
mini = 500
maxa = 1500

#Create three choices that are unique (and not equal to the answer)
choice_list = random.sample(range(mini,maxa),3)
while choice_list.count(int(answer)) >= 1:
    choice_list = random.sample(range(mini,maxa),3)

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = '{:.2f}'.format(answer) + " m"
option_2 = '{:.2f}'.format(choice_list[0]) + " m"
option_3 = '{:.2f}'.format(choice_list[1]) + " m"
option_4 = '{:.2f}'.format(choice_list[2]) + " m"

multiple_choice(option_1, option_2, option_3, option_4)

button3 = widgets.Button(description="Generate New Question", layout=Layout(width='20%', height='100%'))
button3.on_click(run_all)
display(button3)

#Variables to randomize
change_length = round(random.uniform(1.0, 4.0), 2)
initial_length = round(random.uniform(100.0, 150.0), 1)

#Dictionary of different materials
materials = {"aluminum": 0.000025, "iron": 0.000012, "steel": 0.000012, "glass (soft)": 0.000009, "glass (pyrex)": 0.000003, "concrete": 0.000012, "platinum": 0.000009, "copper": 0.000016}
chosen_material = random.choice(list(materials.keys()))

#Print question
question = "By how much would you need to heat a {} inch {} sample to make it expand by {} inches? Round to the nearest degree Celsius.".format(initial_length, chosen_material, change_length)
print(question)

#Answer calculation
answer = int(change_length / (materials[chosen_material] * initial_length))

#Define range of values for random multiple choices
mini = 600
maxa = 3000

#Create three choices that are unique (and not equal to the answer)
choice_list = random.sample(range(mini,maxa),3)
while choice_list.count(answer) >= 1:
    choice_list = random.sample(range(mini,maxa),3)

#Assign each multiple choice to these four variables
#Option_1 contains the answer
option_1 = str(answer) + " °C"
option_2 = str(choice_list[0]) + " °C"
option_3 = str(choice_list[1]) + " °C"
option_4 = str(choice_list[2]) + " °C"

multiple_choice(option_1, option_2, option_3, option_4)

## Application: Aircraft Components

Aircraft materials often require *specialized properties* in order to operate in the most desirable manner. The specialized property of the material may be the most important consideration in materials selection. Listed below are several key specialized properties considered in aircraft materials selection:

- Electrical conductivity (important for the outer skin of the aircraft)
- Stealth (materials that can absorb radar waves and/or reduce the infrared visibility are used in the external surface of covert military aircraft)
- Thermal conductivity (used in high-temperature applications, such as heat shields and engine components)
- Thermal expansion (used in high-temperature range applications, such as wing and engine components)

### Problem

An unknown metal alloy is being tested to discover its thermal properties to see if it suitable for use as a *spar*, which is a component of an airplane wing (shown in red below). The alloy is formed into a bar measuring 1.00 metre in length and it is then heated from its starting temperature of 30 °C to a final temperature of 100.0 °C. The length of the heated bar is measured to be exactly 1.002 metres in length. **What is the coefficient of thermal expansion of the alloy? Round your answer to two significant figures.**

option_1 = str(0.000029) + " °C" + chr(0x207B) + chr(0x00B9)
option_2 = str(0.014) + " °C" + chr(0x207B) + chr(0x00B9)
option_3 = str(0.000032) + " °C" + chr(0x207B) + chr(0x00B9)
option_4 = str(0.0140) + " °C" + chr(0x207B) + chr(0x00B9)

multiple_choice(option_1, option_2, option_3, option_4)

The aircraft wing (from above) experiences temperature extremes that span 210 ℃. The spar for the wing will have a length of 18.0 metres. Testing indicates that the aircraft wing will remain stable only if the spar never expands to a length larger than 18.103 metres. **If the component is made from the metal alloy in question, will it meet this requirement?**

**Hint:** Use the sliders below to calculate the linear expansion of the spar.

################ Widgets #################
app_out = Output()
style = {'description_width': 'initial'}
temp_span = IntSlider(continuous_update=False, wait=True, value=0, min=0, max=500, step=1, description=chr(0x0394) + 'T (°C)')
initialL_alloy = widgets.FloatSlider(continuous_update=False, wait=True, value=0, min=0, max=30, step=1, readout_format='.1f', description= 'L (m)', style=style)
linear_constant = widgets.FloatSlider(continuous_update=False, wait=True, value=0, min=0, max=10, step=0.1, readout_format='.1f', description= chr(0x03b1) + " (1" + chr(0x2A09) + "10" + chr(0x207B) + chr(0x2075) + " , " + chr(0x2103) + chr(0x207B) + chr(0x00B9) + ")", style=style)
calc_change = widgets.Button(description="Calculate Linear Expansion", layout=Layout(width='20%', height='88%'),button_style='success')

############## Functions ################
def initial_spar():
    layout = go.Layout(title = "Unknown Alloy (before heat is applied)", scene = dict(
                    xaxis = dict(title='', range = [0,5],
                         backgroundcolor="rgb(200, 200, 230)",
                         gridcolor="rgb(255, 255, 255)",
                         showbackground=True,
                         zerolinecolor="rgb(255, 255, 255)",
                         showticklabels=False,),
                    yaxis = dict(title='', range = [0,5],
                        backgroundcolor="rgb(230, 200,230)",
                        gridcolor="rgb(255, 255, 255)",
                        showbackground=True,
                        zerolinecolor="rgb(255, 255, 255)",
                        showticklabels=False),
                    zaxis = dict(title='', range = [0,18],
                        backgroundcolor="rgb(230, 230,200)",
                        gridcolor="rgb(255, 255, 255)",
                        showbackground=True,
                        zerolinecolor="rgb(255, 255, 255)",
                        showticklabels=False,),),
                  )
    data = [go.Mesh3d(
            x = [2, 2, 4, 4, 2, 2, 4, 4],
            y = [2, 3, 3, 2, 2, 3, 3, 2],
            z = [0, 0, 0, 0, 18, 18, 18, 18],
            i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
            j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
            k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
            name='Unknown Alloy',
            color='#000066',
            text = "Spar (wing component; unknown alloy)", hoverinfo='text',
        )]
    config={'showLink': False, 'editable': False}
    fig = go.Figure(data=data, layout=layout)
    iplot(fig, config=config)

def draw_spar(new, i_height):
    clear_output(wait=True)
    layout = go.Layout(title = chr(0x0394) + 'L (Unknown Alloy) = ' + str(round(new * 100 ,5)) + ' cm', scene = dict(
                    xaxis = dict(title='', range = [0,5],
                         backgroundcolor="rgb(200, 200, 230)",
                         gridcolor="rgb(255, 255, 255)",
                         showbackground=True,
                         zerolinecolor="rgb(255, 255, 255)",
                         showticklabels=False,),
                    yaxis = dict(title='', range = [0,5],
                        backgroundcolor="rgb(230, 200,230)",
                        gridcolor="rgb(255, 255, 255)",
                        showbackground=True,
                        zerolinecolor="rgb(255, 255, 255)",
                        showticklabels=False),
                    zaxis = dict(title='', range = [0,18],
                        backgroundcolor="rgb(230, 230,200)",
                        gridcolor="rgb(255, 255, 255)",
                        showbackground=True,
                        zerolinecolor="rgb(255, 255, 255)",
                        showticklabels=False,),),
                  )
    data = [go.Mesh3d(
            x = [2, 2, 4, 4, 2, 2, 4, 4],
            y = [2, 3, 3, 2, 2, 3, 3, 2],
            z = [0, 0, 0, 0, 18, 18, 18, 18],
            i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
            j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
            k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
            name='Unknown Alloy',
            color='#000066',
            text = "Initial length (L) = " + str(i_height) + ' m', hoverinfo='text',
        ), go.Mesh3d(
            x = [2, 2, 4, 4, 2, 2, 4, 4],
            y = [2, 3, 3, 2, 2, 3, 3, 2],
            z = [18, 18, 18, 18, 18+new, 18+new, 18+new, 18+new],
            i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
            j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
            k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
            name='Thermal Expansion',
            color = "#CC0000",
            text = "Thermal Expansion ("+chr(0x0394)+"L) = " + str(round(new,5)) + ' m', hoverinfo='text',
        )]
    config={'showLink': False, 'editable': False}
    fig = go.Figure(data=data, layout=layout)
    iplot(fig, config=config)

def on_submit_button_clicked(b):
    alloy_change = temp_span.value * initialL_alloy.value * (linear_constant.value * 0.00001)
    with app_out:
        draw_spar(alloy_change, initialL_alloy.value)

######### Display #########
display(VBox([temp_span, initialL_alloy, linear_constant]))
display(calc_change)
display(app_out)

with app_out:
    initial_spar()

calc_change.on_click(on_submit_button_clicked)

option_1 = "The final length of the spar is 18.10962 m; therefore, it fails the requirement."
option_2 = "The final length of the spar is 10.962 cm; therefore, it passes the requirement."
option_3 = "The final length of the spar is 18.103 m; therefore, it passes the requirement."
option_4 = "The final length of the spar is 28.962 m; therefore, it fails the requirement."

multiple_choice(option_1, option_2, option_3, option_4)

## Conclusion

- Thermal Linear Expansion is calculated using the formula: $\Delta L = \alpha L \Delta T$.
- The coefficient of linear expansion ($\alpha$) is experimentally calculated. The experiment involves measuring $\Delta L$ and solving for $\alpha$ using the above formula.
- If the material is known (such as steel, concrete, iron, etc.) you can reference the material's coefficient of linear expansion using a Table of Coefficient of Linear Expansion.
- There are many applications to being able to predict thermal expansion, such as the building of bridges, skyscrapers, airplanes, cars, and piping (to name a few).
- This notebook covered *linear* thermal expansion. If you are interested in applying what you have learned or gaining further knowledge of material science, you're next step is to study *area expansion* and *volume expansion*, which will explore the 2-dimensional and 3-dimensional expansion of materials.

[![Callysto.ca License](https://github.com/callysto/curriculum-notebooks/blob/master/callysto-notebook-banner-bottom.jpg?raw=true)](https://github.com/callysto/curriculum-notebooks/blob/master/LICENSE.md)


# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.11.3
#   kernelspec:
#     display_name: 'Python 3.6.7 64-bit (''base'': conda)'
#     name: python367jvsc74a57bd050da0f6fa72fb86d21724871d314354b884db45bd357078f1680189ca335f685
# ---

# + [markdown] id="view-in-github" colab_type="text"
#

# + [markdown] id="Yn51eYujm5S1"
# # Text preprocessing
#
# We discuss how to convert a sequence of words or characters into numeric form, which can then be fed into an ML model.
#
#
#

# + id="ysx0t0REm4r0"
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(seed=1)
import math

import torch
from torch import nn
from torch.nn import functional as F
from torch.utils import data

# !mkdir figures # for saving plots

# + id="V6Jbluorndzr"
import collections
import re
import random
import os
import requests
import zipfile
import hashlib

# +
# Required functions for downloading data

def download(name, cache_dir=os.path.join('..', 'data')):
    """Download a file inserted into DATA_HUB, return the local filename."""
    assert name in DATA_HUB, f"{name} does not exist in {DATA_HUB}."
    url, sha1_hash = DATA_HUB[name]
    os.makedirs(cache_dir, exist_ok=True)
    fname = os.path.join(cache_dir, url.split('/')[-1])
    if os.path.exists(fname):
        sha1 = hashlib.sha1()
        with open(fname, 'rb') as f:
            while True:
                data = f.read(1048576)
                if not data:
                    break
                sha1.update(data)
        if sha1.hexdigest() == sha1_hash:
            return fname  # Hit cache
    print(f'Downloading {fname} from {url}...')
    r = requests.get(url, stream=True, verify=True)
    with open(fname, 'wb') as f:
        f.write(r.content)
    return fname

def download_extract(name, folder=None):
    """Download and extract a zip/tar file."""
    fname = download(name)
    base_dir = os.path.dirname(fname)
    data_dir, ext = os.path.splitext(fname)
    if ext == '.zip':
        fp = zipfile.ZipFile(fname, 'r')
    elif ext in ('.tar', '.gz'):
        fp = tarfile.open(fname, 'r')
    else:
        assert False, 'Only zip/tar files can be extracted.'
    fp.extractall(base_dir)
    return os.path.join(base_dir, folder) if folder else data_dir

# + [markdown] id="e9vbpUMwTRY1"
# # Basics
#
# This section is based on sec 8.2 of http://d2l.ai/chapter_recurrent-neural-networks/text-preprocessing.html
#

# + [markdown] id="RMrGxkRNnOx_"
# ## Data
#
# As a simple example, we use the book "The Time Machine" by H G Wells, since it is short (30k words) and public domain.

# + colab={"base_uri": "https://localhost:8080/"} id="D7OJT7o8nDQN" outputId="2dd7e687-6b9a-48d2-ab49-e1bb5b64d41b"
DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'

DATA_HUB['time_machine'] = (DATA_URL + 'timemachine.txt',
                                '090b5e7e70c295757f55df93cb0a180b9691891a')

def read_time_machine():
    """Load the time machine dataset into a list of text lines."""
    with open(download('time_machine'), 'r') as f:
        lines = f.readlines()
    return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]

lines = read_time_machine()
print(f'number of lines: {len(lines)}')

# + colab={"base_uri": "https://localhost:8080/"} id="uCsuaurvnlK8" outputId="8dcf484e-8f45-4748-cc93-c2a2ada427d2"
for i in range(11):
  print(i, lines[i])

# + colab={"base_uri": "https://localhost:8080/"} id="btVyl4dItGVT" outputId="6b67aec4-4c26-43f7-ea6e-0c7fd1440f55"
nchars = 0
nwords = 0
for i in range(len(lines)):
  nchars += len(lines[i])
  words = lines[i].split()
  nwords += len(words)
print('total num characters ', nchars)
print('total num words ', nwords)

# + [markdown] id="KKBbwDcKnwsA"
# ## Tokenization

# + colab={"base_uri": "https://localhost:8080/"} id="X32lM-XvnxhC" outputId="4783ff38-b282-4b0e-fc59-996f6ec0d6a6"
def tokenize(lines, token='word'):
    """Split text lines into word or character tokens."""
    if token == 'word':
        return [line.split() for line in lines]
    elif token == 'char':
        return [list(line) for line in lines]
    else:
        print('ERROR: unknown token type: ' + token)

tokens = tokenize(lines)
for i in range(11):
    print(tokens[i])

# + [markdown] id="X-Tbg9jjn8XN"
# ## Vocabulary
#
# We map each word to a unique integer id, sorted by decreasing frequency.
# We reserve the special id of 0 for the "unknown word".
# We also allow for a list of reserved tokens, such as “pad" for padding, "bos" to present the beginning for a sequence, and “eos” for the end of a sequence.
#

# + id="8ZOLrVNon9dk"
class Vocab:
    """Vocabulary for text."""
    def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
        if tokens is None:
            tokens = []
        if reserved_tokens is None:
            reserved_tokens = []
        # Sort according to frequencies
        counter = count_corpus(tokens)
        self.token_freqs = sorted(counter.items(), key=lambda x: x[1],
                                  reverse=True)
        # The index for the unknown token is 0
        self.unk, uniq_tokens = 0, ['<unk>'] + reserved_tokens
        uniq_tokens += [
            token for token, freq in self.token_freqs
            if freq >= min_freq and token not in uniq_tokens]
        self.idx_to_token, self.token_to_idx = [], dict()
        for token in uniq_tokens:
            self.idx_to_token.append(token)
            self.token_to_idx[token] = len(self.idx_to_token) - 1

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
            return self.token_to_idx.get(tokens, self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self, indices):
        if not isinstance(indices, (list, tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]

def count_corpus(tokens):
    """Count token frequencies."""
    # Here `tokens` is a 1D list or 2D list
    if len(tokens) == 0 or isinstance(tokens[0], list):
        # Flatten a list of token lists into a list of tokens
        tokens = [token for line in tokens for token in line]
    return collections.Counter(tokens)

# + [markdown] id="CV0rTlaqoSNE"
# Here are the top 10 words (and their codes) in our corpus.

# + colab={"base_uri": "https://localhost:8080/"} id="tYmbCwY6oUFB" outputId="31a05a85-5113-4db8-aacf-f944f2c576f8"
vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[:10])

# + [markdown] id="sKXJQdbXoiqT"
# Here is a tokenization of a few sentences.

# + colab={"base_uri": "https://localhost:8080/"} id="jd73-1zzoUWo" outputId="f2e7dbda-4053-4773-d385-686f6c549144"
for i in [0, 10]:
    print('words:', tokens[i])
    print('indices:', vocab[tokens[i]])

# + [markdown] id="-6LsXchMop3u"
# ## Putting it altogether
#
# We tokenize the corpus at the character level, and return the sequence of integers, as well as the corresponding Vocab object.

# + id="1BywQ9iUoq_D"
def load_corpus_time_machine(max_tokens=-1):
    """Return token indices and the vocabulary of the time machine dataset."""
    lines = read_time_machine()
    tokens = tokenize(lines, 'char')
    vocab = Vocab(tokens)
    # Since each text line in the time machine dataset is not necessarily a
    # sentence or a paragraph, flatten all the text lines into a single list
    corpus = [vocab[token] for line in tokens for token in line]
    if max_tokens > 0:
        corpus = corpus[:max_tokens]
    return corpus, vocab

# + colab={"base_uri": "https://localhost:8080/"} id="oQzX4Am8osdh" outputId="53318718-5dba-4574-8f7d-3de538584c0d"
corpus, vocab = load_corpus_time_machine()
len(corpus), len(vocab)

# + colab={"base_uri": "https://localhost:8080/"} id="IgDxt_PRovAb" outputId="7aaffeaa-0b08-4796-bce7-e3ef6fe9cc58"
print(corpus[:20])

# + colab={"base_uri": "https://localhost:8080/"} id="egONc6CRowLa" outputId="030047c5-7199-4000-9e0c-39ba75275fd6"
print(list(vocab.token_to_idx.items())[:10])

# + colab={"base_uri": "https://localhost:8080/"} id="9xKwPjAAozaX" outputId="68c7959c-7bfe-42a1-f324-b9b4a1a90113"
print([vocab.idx_to_token[i] for i in corpus[:20]])

# + [markdown] id="X3fLUodCZebY"
# ## One-hot encodings
#
# We can convert a sequence of N integers into a N*V one-hot matrix, where V is the vocabulary size.

# + colab={"base_uri": "https://localhost:8080/"} id="Qk21iCFhZj89" outputId="b5323706-52f8-459b-b382-70acbf54ba26"
x = torch.tensor(corpus[:3])
print(x)
X = F.one_hot(x, len(vocab))
print(X.shape)
print(X)

# + [markdown] id="8vO99OOSuYhX"
# # Language modeling
#
# When fitting language models, we often need to chop up a long sequence into a set of short sequences, which may be overlapping, as shown below, where we extract subsequences of length $n=5$.
#
#
#
# Below we show how to do this.
#
# This section is based on sec 8.3.4 of
# http://d2l.ai/chapter_recurrent-neural-networks/language-models-and-dataset.html#reading-long-sequence-data
#

# + [markdown] id="Vert2-4qw5K7"
# ## Random ordering

# + [markdown] id="_rARqDyZuvlu"
# To increase variety of the data, we can start the extraction at a random offset. We can thus create a random sequence data iterator, as follows.
#

# + id="meuw3vkjpL22"
def seq_data_iter_random(corpus, batch_size, num_steps):
    """Generate a minibatch of subsequences using random sampling."""
    # Start with a random offset (inclusive of `num_steps - 1`) to partition a
    # sequence
    corpus = corpus[random.randint(0, num_steps - 1):]
    # Subtract 1 since we need to account for labels
    num_subseqs = (len(corpus) - 1) // num_steps
    # The starting indices for subsequences of length `num_steps`
    initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
    # In random sampling, the subsequences from two adjacent random
    # minibatches during iteration are not necessarily adjacent on the
    # original sequence
    random.shuffle(initial_indices)

    def data(pos):
        # Return a sequence of length `num_steps` starting from `pos`
        return corpus[pos:pos + num_steps]

    num_batches = num_subseqs // batch_size
    for i in range(0, batch_size * num_batches, batch_size):
        # Here, `initial_indices` contains randomized starting indices for
        # subsequences
        initial_indices_per_batch = initial_indices[i:i + batch_size]
        X = [data(j) for j in initial_indices_per_batch]
        Y = [data(j + 1) for j in initial_indices_per_batch]
        yield torch.tensor(X), torch.tensor(Y)

# + [markdown] id="71kdus7mvMFQ"
# For example, let us generate a sequence 0,1,..,34, and then extract subsequences of length 5. Each minibatch will have 2 such subsequences, starting at random offsets. There is no ordering between the subsequences, either within or across minibatches. There are $\lfloor (35-1)/5 \rfloor = 6$ such subsequences, so the iterator will generate 3 minibatches, each of size 2.
#
# For language modeling tasks, we define $X$ to be the first $n-1$ tokens, and $Y$ to be the $n$'th token, which is the one to be predicted.

# + colab={"base_uri": "https://localhost:8080/"} id="x8GXyqOgvOI7" outputId="efc1667a-624e-461c-a72c-c9d8ac244f81"
my_seq = list(range(35))
b = 0
for X, Y in seq_data_iter_random(my_seq, batch_size=2, num_steps=5):
    print('batch: ', b)
    print('X: ', X, '\nY:', Y)
    b += 1

# + [markdown] id="wdg490Gow7la"
# ## Sequential ordering

# + [markdown] id="55ECVkQLwL8K"
# We can also require that the $i$'th subsequence in minibatch $b$ follows the $i$'th subsequence in minibatch $b-1$. This is useful when training RNNs, since when the model encounters batch $b$, the hidden state of the model will already be initialized by the last token in sequence $i$ of batch $b-1$.

# + id="r3uVV7lYwCdv"
def seq_data_iter_sequential(corpus, batch_size, num_steps):
    """Generate a minibatch of subsequences using sequential partitioning."""
    # Start with a random offset to partition a sequence
    offset = random.randint(0, num_steps)
    num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
    Xs = torch.tensor(corpus[offset:offset + num_tokens])
    Ys = torch.tensor(corpus[offset + 1:offset + 1 + num_tokens])
    Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
    num_batches = Xs.shape[1] // num_steps
    for i in range(0, num_steps * num_batches, num_steps):
        X = Xs[:, i:i + num_steps]
        Y = Ys[:, i:i + num_steps]
        yield X, Y

# + [markdown] id="KGRIkFvXwZX6"
# Below we give an example. We see that the first subsequence in batch 1
# is [0,1,2,3,4], and the first subsequence in batch 2 is [5,6,7,8,9], as desired.

# + colab={"base_uri": "https://localhost:8080/"} id="aLQzm2qrwY0m" outputId="27a0f0e7-46db-469c-dd1f-906b599be01d"
for X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5):
    print('X: ', X, '\nY:', Y)

# + [markdown] id="SP96EBA-w9MF"
# ## Data iterator
# -

def load_corpus_time_machine(max_tokens=-1):
    """Return token indices and the vocabulary of the time machine dataset."""
    lines = read_time_machine()
    tokens = tokenize(lines, 'char')
    vocab = Vocab(tokens)
    # Since each text line in the time machine dataset is not necessarily a
    # sentence or a paragraph, flatten all the text lines into a single list
    corpus = [vocab[token] for line in tokens for token in line]
    if max_tokens > 0:
        corpus = corpus[:max_tokens]
    return corpus, vocab

# + id="IpjIv8tMw-QD"
class SeqDataLoader:  #@save
    """An iterator to load sequence data."""
    def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
        if use_random_iter:
            self.data_iter_fn = seq_data_iter_random
        else:
            self.data_iter_fn = seq_data_iter_sequential
        self.corpus, self.vocab = load_corpus_time_machine(max_tokens)
        self.batch_size, self.num_steps = batch_size, num_steps

    def __iter__(self):
        return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)

# + id="pIy_YUk9w-0A"
def load_data_time_machine(batch_size, num_steps,  #@save
                           use_random_iter=False, max_tokens=10000):
    """Return the iterator and the vocabulary of the time machine dataset."""
    data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter,
                              max_tokens)
    return data_iter, data_iter.vocab

# + id="Y44o8-MkxA3t"
data_iter, vocab = load_data_time_machine(2, 5)

# + colab={"base_uri": "https://localhost:8080/"} id="sf-py0roxmAC" outputId="66691d42-4905-4fe3-b906-8856e525665b"
print(list(vocab.token_to_idx.items())[:10])

# + colab={"base_uri": "https://localhost:8080/"} id="6XhwWfMHxXTA" outputId="a29066f2-d2e2-447c-d290-ea473ecc0ee1"
b = 0
for X, Y in data_iter:
    print('batch: ', b)
    print('X: ', X, '\nY:', Y)
    b += 1
    if b > 2:
      break

# + [markdown] id="yDmK1xQ9T4IY"
# # Machine translation
#
# When dealing with sequence-to-sequence tasks, such as NMT, we need to create a vocabulary for the source and target language. In addition, the input and output sequences may have different lengths, so we need to use padding to ensure that we can create fixed-size minibatches. We show how to do this below.
#
# This is based on sec 9.5 of
# http://d2l.ai/chapter_recurrent-modern/machine-translation-and-dataset.html
#
#
#

# + [markdown] id="gBUgcAcmUdCJ"
# ## Data
#
# We use an English-French dataset that consists of bilingual sentence pairs from the [Tatoeba Project](http://www.manythings.org/anki/). Each line in the dataset is a tab-delimited pair of an English text sequence (source) and the translated French text sequence (target).
#

# + colab={"base_uri": "https://localhost:8080/"} id="UnjXAtdYUUW8" outputId="66c19d66-217b-43ef-9877-854ea32725d0"
DATA_HUB['fra-eng'] = (DATA_URL + 'fra-eng.zip',
                           '94646ad1522d915e7b0f9296181140edcf86a4f5')

def read_data_nmt():
    """Load the English-French dataset."""
    data_dir = download_extract('fra-eng')
    with open(os.path.join(data_dir, 'fra.txt'), 'r') as f:
        return f.read()

raw_text = read_data_nmt()
print(raw_text[:100])

# + [markdown] id="sqZImjzDVMHa"
# ## Preprocessing
#
# We apply several preprocessing steps: we replace non-breaking space with space, convert uppercase letters to lowercase ones, and insert space between words and punctuation marks.
#

# + colab={"base_uri": "https://localhost:8080/"} id="r5ZUH4ZaUquY" outputId="316a660f-1d3a-45e4-ddb2-f97ed3128733"
def preprocess_nmt(text):
    """Preprocess the English-French dataset."""
    def no_space(char, prev_char):
        return char in set(',.!?') and prev_char != ' '

    # Replace non-breaking space with space, and convert uppercase letters to
    # lowercase ones
    text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()
    # Insert space between words and punctuation marks
    out = [
        ' ' + char if i > 0 and no_space(char, text[i - 1]) else char
        for i, char in enumerate(text)]
    return ''.join(out)

text = preprocess_nmt(raw_text)
print(text[:110])

# + [markdown] id="yGChAGPjVgUn"
# We tokenize at the word level.  The following tokenize_nmt function tokenizes the the first `num_examples` text sequence pairs, where each token is either a word or a punctuation mark.

# + id="PZ-iR79zVKM_"
def tokenize_nmt(text, num_examples=None):
    """Tokenize the English-French dataset."""
    source, target = [], []
    for i, line in enumerate(text.split('\n')):
        if num_examples and i > num_examples:
            break
        parts = line.split('\t')
        if len(parts) == 2:
            source.append(parts[0].split(' '))
            target.append(parts[1].split(' '))
    return source, target

# + colab={"base_uri": "https://localhost:8080/"} id="v282HIgRVfza" outputId="12cafea1-eee6-43e9-ba8d-79c6ef5affc2"
source, target = tokenize_nmt(text)
source[:10], target[:10]

# + [markdown] id="LC8u2YndV6-P"
# ## Vocabulary
#
# We can make a source and target vocabulary. To avoid having too many unique tokens, we specify a minimum frequency of 2 - all others will get replaced by "unk". We also add special tags for padding, begin of sentence, and end of sentence.

# + colab={"base_uri": "https://localhost:8080/"} id="v9SrJQc3VtSn" outputId="8b0e6cd5-890d-46c5-ce79-14f256ba63b3"
src_vocab = Vocab(source, min_freq=2,
                      reserved_tokens=['<pad>', '<bos>', '<eos>'])
len(src_vocab)

# + colab={"base_uri": "https://localhost:8080/"} id="KPF2FthfV840" outputId="f08bca8e-8564-411d-e58f-6e5b2e19f671"
# French has more high frequency words than English
target_vocab = Vocab(target, min_freq=2,
                      reserved_tokens=['<pad>', '<bos>', '<eos>'])
len(target_vocab)

# + [markdown] id="d0DyArAtWcob"
# ## Truncation and padding
#
# To create minibatches of sequences, all of the same length, we truncate sentences that are too long, and pad ones that are too short.

# + colab={"base_uri": "https://localhost:8080/"} id="x2D62tczWP-2" outputId="25ca1de2-97a6-4273-abbf-644a41562bb8"
def truncate_pad(line, num_steps, padding_token):
    """Truncate or pad sequences."""
    if len(line) > num_steps:
        return line[:num_steps]  # Truncate
    return line + [padding_token] * (num_steps - len(line))  # Pad

print(truncate_pad(source[0], 10, 'pad'))
print(truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>']))

# + id="RgyPxL6tWvJC"
def build_array_nmt(lines, vocab, num_steps):
    """Transform text sequences of machine translation into minibatches."""
    lines = [vocab[l] for l in lines]
    lines = [l + [vocab['<eos>']] for l in lines]
    array = torch.tensor([
        truncate_pad(l, num_steps, vocab['<pad>']) for l in lines])
    valid_len = (array != vocab['<pad>']).type(torch.int32).sum(1)
    return array, valid_len

# + colab={"base_uri": "https://localhost:8080/"} id="SlwULfBwW9ma" outputId="51eae96b-bfd0-4325-93ef-c98315abb6b0"
num_steps = 10
src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps)
print(src_array.shape)
print(src_valid_len.shape)

# + colab={"base_uri": "https://localhost:8080/"} id="br6L3nDbXFHY" outputId="e70924a1-6e71-49dc-b393-364feac92296"
print(src_array[0,:]) # go, ., eos, pad, ..., pad
print(src_valid_len[0])

# + [markdown] id="UyXmgFUvXVnA"
# ## Data iterator
#
# Below we combine all of the above pieces into a handy function.

# + id="AkD1QMiJXKAP"
def load_array(data_arrays, batch_size, is_train=True):
    """Construct a PyTorch data iterator."""
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)

def load_data_nmt(batch_size, num_steps, num_examples=600):
    """Return the iterator and the vocabularies of the translation dataset."""
    text = preprocess_nmt(read_data_nmt())
    source, target = tokenize_nmt(text, num_examples)
    src_vocab = Vocab(source, min_freq=2,
                          reserved_tokens=['<pad>', '<bos>', '<eos>'])
    tgt_vocab = Vocab(target, min_freq=2,
                          reserved_tokens=['<pad>', '<bos>', '<eos>'])
    src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps)
    tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps)
    data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)
    data_iter = load_array(data_arrays, batch_size)
    return data_iter, src_vocab, tgt_vocab

# + [markdown] id="ARsiX21oXdOd"
# Show the first minibatch.

# + colab={"base_uri": "https://localhost:8080/"} id="vl00eydyXeYF" outputId="a48df679-efd7-4e87-b3cd-fb66689e56e0"
train_iter, src_vocab, tgt_vocab = load_data_nmt(batch_size=2, num_steps=8)
for X, X_valid_len, Y, Y_valid_len in train_iter:
    print('X:', X.type(torch.int32))
    print('valid lengths for X:', X_valid_len)
    print('Y:', Y.type(torch.int32))
    print('valid lengths for Y:', Y_valid_len)
    break

# + id="thnQxtaIXenj"