{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
" \n",
" "
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
" \n",
" "
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import sys\n",
"sys.path.append(\"../\")\n",
"sys.path.append(\"../RecipeAnalysis/\")\n",
"\n",
"import settings\n",
"\n",
"import pycrfsuite\n",
"\n",
"import json\n",
"\n",
"import db.db_settings as db_settings\n",
"from db.database_connection import DatabaseConnection\n",
"\n",
"from Tagging.conllu_generator import ConlluGenerator\n",
"from Tagging.crf_data_generator import *\n",
"\n",
"from RecipeAnalysis.Recipe import Ingredient\n",
"\n",
"from difflib import SequenceMatcher\n",
"\n",
"import numpy as np\n",
"\n",
"import plotly.graph_objs as go\n",
"from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot\n",
"from plotly.subplots import make_subplots\n",
"init_notebook_mode(connected=True)\n",
"\n",
"from graphviz import Digraph\n",
"\n",
"import itertools\n",
"\n",
"import random\n",
"\n",
"import plotly.io as pio\n",
"pio.renderers.default = \"jupyterlab\"\n",
"\n",
"from IPython.display import Markdown, HTML, display\n",
"\n",
"from copy import deepcopy"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import pickle\n",
"m_act = pickle.load(open(\"m_act.pickle\", \"rb\"))\n",
"m_mix = pickle.load(open(\"m_mix.pickle\", \"rb\"))\n",
"m_base_act = pickle.load(open(\"m_base_act.pickle\", \"rb\"))\n",
"m_base_mix = pickle.load(open(\"m_base_mix.pickle\", \"rb\"))\n",
"\n",
"c_act = m_act.get_csr()\n",
"c_mix = m_mix.get_csr()\n",
"c_base_act = m_base_act.get_csr()\n",
"c_base_mix = m_base_mix.get_csr()\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"actions = m_act.get_labels()[0]"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"base_ingredients = m_base_mix.get_labels()"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"def get_sym_adjacent(key, m, c):\n",
" index = m._label_index[key]\n",
" i1 = c[index,:].nonzero()[1]\n",
" i2 = c[:,index].nonzero()[0]\n",
" \n",
" i = np.concatenate((i1,i2))\n",
" \n",
" names = np.array(m.get_labels())[i]\n",
" \n",
" counts = np.concatenate((c[index, i1].toarray().flatten(), c[i2, index].toarray().flatten()))\n",
" \n",
" s = np.argsort(-counts)\n",
" \n",
" return names[s], counts[s]"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"def get_forward_adjacent(key, m, c):\n",
" index = m._x_label_index[key]\n",
" i = c[index,:].nonzero()[1]\n",
" \n",
" names = np.array(m._y_labels)[i]\n",
" \n",
" counts = c[index, i].toarray().flatten()\n",
" \n",
" s = np.argsort(-counts)\n",
" \n",
" return names[s], counts[s]"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"def get_backward_adjacent(key, m, c):\n",
" index = m._y_label_index[key]\n",
" i = c[:,index].nonzero()[0]\n",
" \n",
" names = np.array(m._x_labels)[i]\n",
" \n",
" counts = c[i, index].toarray().flatten()\n",
" \n",
" s = np.argsort(-counts)\n",
" \n",
" return names[s], counts[s]"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"def sym_sum(key, m, c):\n",
" return np.sum(get_sym_adjacent(key,m,c)[1])\n",
"\n",
"def fw_sum(key, m, c):\n",
" return np.sum(get_forward_adjacent(key,m,c)[1])\n",
"\n",
"def bw_sum(key, m, c):\n",
" return np.sum(get_backward_adjacent(key,m,c)[1])"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"def sym_score(key_a, key_b, m, c):\n",
"\n",
" ia = m._label_index[key_a]\n",
" ib = m._label_index[key_b]\n",
" \n",
" v = c[ia,ib] + c[ib,ia]\n",
" \n",
" if v == 0:\n",
" return 0\n",
" \n",
" return max((v/sym_sum(key_a, m, c)), (v/sym_sum(key_b, m, c)))\n",
"\n",
"def asym_score(key_a, key_b, m, c):\n",
" ia = m._x_label_index[key_a]\n",
" ib = m._y_label_index[key_b]\n",
" \n",
" v = c[ia,ib]\n",
" \n",
" if v == 0:\n",
" return 0\n",
" \n",
" return max(v/fw_sum(key_a, m, c), v/bw_sum(key_b, m, c))"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"class RecipeTreeNode(object):\n",
" \n",
" id = 0\n",
" \n",
" def __init__(self, name, constant=False, single_child=False):\n",
" self._constant = constant\n",
" self._name = name\n",
" self._parent = None\n",
" \n",
" self._id = str(RecipeTreeNode.id)\n",
" RecipeTreeNode.id += 1\n",
" \n",
" self._single_child = single_child\n",
" \n",
" if self._single_child:\n",
" self._child = None\n",
" \n",
" def child():\n",
" return self._child\n",
" \n",
" def remove_child(c):\n",
" assert c == self._child\n",
" self._child._parent = None\n",
" self._child = None\n",
" \n",
" def childs():\n",
" c = self.child()\n",
" if c is None:\n",
" return set()\n",
" return set([c])\n",
" \n",
" def add_child(n):\n",
" self._child = n\n",
" n._parent = self\n",
" \n",
" self.child = child\n",
" self.childs = childs\n",
" self.add_child = add_child\n",
" self.remove_child = remove_child\n",
" else:\n",
" self._childs = set()\n",
" \n",
" def childs():\n",
" return self._childs\n",
" \n",
" def add_child(n):\n",
" self._childs.add(n)\n",
" n._parent = self\n",
" \n",
" def remove_child(c):\n",
" assert c in self._childs\n",
" c._parent = None\n",
" self._childs.remove(c)\n",
" \n",
" self.childs = childs\n",
" self.add_child = add_child\n",
" self.remove_child = remove_child\n",
" \n",
" def parent(self):\n",
" return self._parent\n",
" \n",
" def name(self):\n",
" return self._name\n",
" \n",
" def traverse(self):\n",
" l = []\n",
" \n",
" for c in self.childs():\n",
" l += c.traverse()\n",
" \n",
" return [self] + l\n",
" \n",
" def traverse_ingredients(self):\n",
" ingredient_set = []\n",
" for c in self.childs():\n",
" ingredient_set += c.traverse_ingredients()\n",
" \n",
" return ingredient_set\n",
" \n",
" def remove(self):\n",
" p = self.parent()\n",
" childs = self.childs().copy()\n",
" \n",
" assert p is None or not (len(childs) > 1 and p._single_child)\n",
" \n",
" for c in childs:\n",
" self.remove_child(c)\n",
" \n",
" if p is not None:\n",
" p.remove_child(self)\n",
" \n",
" if self._single_child and self._child is not None and p._name == self._child._name:\n",
" # two adjacent nodes with same name would remain after deletion.\n",
" # merge them! (by adding the child's childs to our parent instead of our childs)\n",
" childs = self._child.childs()\n",
" self._child.remove()\n",
" \n",
" \n",
" for c in childs:\n",
" p.add_child(c)\n",
" \n",
" def insert_before(self, n):\n",
" p = self._parent\n",
" if p is not None:\n",
" p.remove_child(self)\n",
" p.add_child(n)\n",
" n.add_child(self)\n",
" \n",
" def mutate(self):\n",
" n_node = self.n_node_mutate_options()\n",
" n_edge = self.n_edge_mutate_options()\n",
" \n",
" choice = random.choice(range(n_node + n_edge))\n",
" if choice < n_node:\n",
" self.mutate_node()\n",
" else:\n",
" self.mutate_edges()\n",
" \n",
" def mutate_edges(self):\n",
" ings = self.traverse_ingredients()\n",
" ing = random.choice(ings)\n",
" \n",
" a, w = get_backward_adjacent(ing._base_ingredient, m_base_act, c_base_act)\n",
" \n",
" action = random.choices(a, w)[0]\n",
" self.insert_before(ActionNode(action))\n",
" \n",
" def mutate_node(self):\n",
" raise NotImplementedError\n",
" \n",
" def n_node_mutate_options(self):\n",
" \n",
" return 0 if self._constant else 1\n",
" \n",
" def n_edge_mutate_options(self):\n",
" n = 1 if self._parent is not None else 0\n",
" return n\n",
" \n",
" def n_mutate_options(self):\n",
" return self.n_edge_mutate_options() + self.n_node_mutate_options()\n",
" \n",
" def dot_node(self, dot):\n",
" raise NotImplementedError()\n",
" \n",
" def dot(self, d=None):\n",
" if d is None:\n",
" d = Digraph()\n",
" self.dot_node(d)\n",
" \n",
" else:\n",
" self.dot_node(d)\n",
" if self._parent is not None:\n",
" d.edge(self._parent._id, self._id)\n",
" \n",
" \n",
" for c in self.childs():\n",
" c.dot(d)\n",
" \n",
" return d\n",
" \n",
" def serialize(self):\n",
" r = {}\n",
" r['type'] = str(self.__class__.__name__)\n",
" r['id'] = self._id\n",
" r['parent'] = self._parent._id if self._parent is not None else None\n",
" r['name'] = self._name\n",
" r['childs'] = [c._id for c in self.childs()]\n",
" r['constant'] = self._constant\n",
" r['single_child'] = self._single_child\n",
" \n",
" return r\n",
" \n",
" def node_score(self):\n",
" raise NotImplementedError()\n",
" \n",
" "
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"class MixNode(RecipeTreeNode):\n",
" def __init__(self, constant=False):\n",
" super().__init__(\"mix\", constant, single_child=False)\n",
" \n",
" def dot_node(self, dot):\n",
" dot.node(self._id, label=f\"{self._name} ({self.node_score()})\", shape=\"diamond\")\n",
" \n",
" def split(self, set_above, set_below, node_between):\n",
" assert len(set_above.difference(self.childs())) == 0\n",
" assert len(set_below.difference(self.childs())) == 0\n",
" \n",
" n_above = MixNode()\n",
" n_below = MixNode()\n",
" \n",
" p = self.parent()\n",
" \n",
" for c in self.childs().copy():\n",
" self.remove_child(c)\n",
" self.remove()\n",
" \n",
" for c in set_below:\n",
" n_below.add_child(c)\n",
" \n",
" for c in set_above:\n",
" n_above.add_child(c)\n",
" \n",
" n_above.add_child(node_between)\n",
" node_between.add_child(n_below)\n",
" \n",
" if p is not None:\n",
" p.add_child(n_above)\n",
" \n",
" # test whether the mix nodes are useless\n",
" if len(n_above.childs()) == 1:\n",
" n_above.remove()\n",
" \n",
" if len(n_below.childs()) == 1:\n",
" n_below.remove()\n",
" \n",
" def n_node_mutate_options(self):\n",
" return 0 if self._constant or len(self.childs()) <= 2 else len(self.childs())\n",
" \n",
" def mutate_node(self):\n",
" \n",
" childs = self.childs()\n",
" \n",
" if len(childs) <= 2:\n",
" print(\"Warning: cannot modify mix node\")\n",
" return\n",
" \n",
" childs = random.sample(childs, len(childs))\n",
" \n",
" n = random.choice(range(1, len(childs)-1))\n",
" \n",
" between_node = ActionNode(random.choice(actions))\n",
" \n",
" self.split(set(childs[:n]), set(childs[n:]), between_node)\n",
" \n",
" \n",
" def node_score(self):\n",
" child_ingredients = [c.traverse_ingredients() for c in self.childs()]\n",
" products = [itertools.product(child_ingredients[i-1],child_ingredients[i]) for i in range(len(child_ingredients))]\n",
" pairwise_tuples = []\n",
" for p in products:\n",
" pairwise_tuples += [x for x in p]\n",
" \n",
" s_base = 0\n",
" s = 0\n",
" \n",
" for ing_a, ing_b in pairwise_tuples:\n",
" try:\n",
" #s_base += sym_score(ing_a._base_ingredient, ing_b._base_ingredient, m_base_mix, c_base_mix)\n",
" s += sym_score(ing_a.to_json(), ing_b.to_json(), m_mix, c_mix)\n",
" except:\n",
" pass\n",
" \n",
" #s_base /= len(pairwise_tuples)\n",
" s /= len(pairwise_tuples)\n",
" \n",
" #return 0.5 * (s_base + s)\n",
" return s\n",
" \n",
" \n",
" \n",
" "
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"class IngredientNode(RecipeTreeNode):\n",
" def __init__(self, name, constant=False):\n",
" super().__init__(name, constant, single_child=True)\n",
" \n",
" def get_actions(self):\n",
" a_set = set()\n",
" n = self.parent()\n",
" while n is not None:\n",
" if type(n) == ActionNode:\n",
" a_set.add(n.name())\n",
" return a_set\n",
" \n",
" def mutate_node(self):\n",
" self._name = random.choice(base_ingredients)\n",
" \n",
" def traverse_ingredients(self):\n",
" return [Ingredient(self._name)]\n",
" \n",
" def node_score(self):\n",
" return 0\n",
" \n",
" \n",
" def dot_node(self, dot):\n",
" dot.node(self._id, label=f\"{self._name} ({self.node_score()})\", shape=\"box\")"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"class ActionNode(RecipeTreeNode):\n",
" def __init__(self, name, constant=False):\n",
" super().__init__(name, constant, single_child=True)\n",
" \n",
" def n_node_mutate_options(self):\n",
" # beacause we can change or remove ourselve!\n",
" return 0 if self._constant else 2 \n",
" def mutate_node(self):\n",
" if random.choice(range(2)) == 0:\n",
" # change action\n",
" self._name = random.choice(actions)\n",
" else:\n",
" # delete\n",
" self.remove()\n",
" \n",
" def traverse_ingredients(self):\n",
" ingredient_set = super().traverse_ingredients()\n",
" for ing in ingredient_set:\n",
" ing.apply_action(self._name)\n",
" \n",
" return ingredient_set\n",
" \n",
" def node_score(self):\n",
" ings = self.child().traverse_ingredients()\n",
" \n",
" s = 0\n",
" \n",
" for ing in ings:\n",
" try:\n",
" score = asym_score(self._name, ing.to_json(), m_act, c_act)\n",
" #base_score = asym_score(self._name, ing._base_ingredient, m_base_act, c_base_act)\n",
" s += score\n",
" except KeyError as e:\n",
" pass\n",
" \n",
" \n",
" return s / len(ings)\n",
" \n",
" def dot_node(self, dot):\n",
" dot.node(self._id, label=f\"{self._name} ({self.node_score()})\", shape=\"ellipse\")"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [],
"source": [
"class Tree(object):\n",
" \n",
" @staticmethod\n",
" def from_ingredients(ingredients: list):\n",
" root = MixNode()\n",
" \n",
" for ing in ingredients:\n",
" root.add_child(IngredientNode(ing, constant=True))\n",
" \n",
" return Tree(root)\n",
" \n",
" @staticmethod\n",
" def from_serialization(s):\n",
" def empty_node(raw_n):\n",
" if raw_n['type'] == \"MixNode\":\n",
" node = MixNode(raw_n['constant'])\n",
" elif raw_n['type'] == \"IngredientNode\":\n",
" node = IngredientNode(raw_n['name'], raw_n['constant'])\n",
" elif raw_n['type'] == \"ActionNode\":\n",
" node = ActionNode(raw_n['name'], raw_n['constant'])\n",
" else:\n",
" print(\"unknown node detected\")\n",
" return\n",
" \n",
" return node\n",
" \n",
" nodes = {}\n",
" for n in s:\n",
" nodes[n['id']] = empty_node(n)\n",
" \n",
" for n in s:\n",
" childs = n['childs']\n",
" id = n['id']\n",
" for c in childs:\n",
" nodes[id].add_child(nodes[c])\n",
" \n",
" return Tree(nodes[s[0]['id']])\n",
" \n",
" \n",
" def __init__(self, root):\n",
" # create a dummy entry node\n",
" self._root = RecipeTreeNode(\"root\", single_child=True)\n",
" self._root.add_child(root)\n",
" \n",
" def root(self):\n",
" return self._root.child()\n",
" \n",
" def mutate(self):\n",
" nodes = self.root().traverse()\n",
" weights = [n.n_mutate_options() for n in nodes]\n",
" \n",
" n = random.choices(nodes, weights)[0]\n",
" \n",
" n.mutate()\n",
" \n",
" def dot(self):\n",
" return self.root().dot()\n",
" \n",
" def serialize(self):\n",
" return [n.serialize() for n in self.root().traverse()]\n",
" \n",
" def structure_score(self):\n",
" n_duplicates = 0\n",
" \n",
" \n",
" \n",
" \n",
" def score(self):\n",
" \n",
" scores = []\n",
" \n",
" nodes = self.root().traverse()\n",
" n_nodes = 0\n",
" s = 0\n",
" for n in nodes:\n",
" if type(n) != IngredientNode:\n",
" scores.append(n.node_score())\n",
" n_nodes += 1\n",
" \n",
" n_duplicates = 0\n",
" seen_actions = set()\n",
" \n",
" for n in nodes:\n",
" if type(n) == ActionNode:\n",
" if n.name() in seen_actions:\n",
" n_duplicates += 1\n",
" else:\n",
" seen_actions.add(n.name())\n",
" \n",
" \n",
" return (sum(scores)/n_nodes) + 1 / (n_duplicates + 1)\n",
" \n",
" def copy(self):\n",
" return Tree.from_serialization(self.serialize())\n"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
"class Population(object):\n",
" def __init__(self, start_ingredients, n_population = 10):\n",
" self.population = [Tree.from_ingredients(start_ingredients) for i in range(n_population)]\n",
" self._n = n_population\n",
" \n",
" def mutate(self):\n",
" for tree in self.population.copy():\n",
" t_clone = tree.copy()\n",
" t_clone.mutate()\n",
" self.population.append(t_clone)\n",
" \n",
" def pairwise_competition(self):\n",
" new_population = []\n",
" random.shuffle(self.population)\n",
" \n",
" for i in range(self._n):\n",
" t_a = self.population[2*i]\n",
" t_b = self.population[2*i+1]\n",
" \n",
" if t_a.score() > t_b.score():\n",
" new_population.append(t_a)\n",
" else:\n",
" new_population.append(t_b)\n",
" \n",
" self.population = new_population\n",
" \n",
" def run(self, n=50):\n",
" for i in range(n):\n",
" print(i)\n",
" self.mutate()\n",
" self.pairwise_competition()\n",
" \n",
" def plot_population(self):\n",
" for t in self.population:\n",
" display(t.root().dot())"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
"p = Population([\"bacon\", \"mushroom\", \"noodle\", \"water\", \"egg\"])"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0\n",
"1\n",
"2\n",
"3\n",
"4\n",
"5\n",
"6\n",
"7\n",
"8\n",
"9\n",
"10\n",
"11\n",
"12\n",
"13\n",
"14\n",
"15\n",
"16\n",
"17\n",
"18\n",
"19\n"
]
}
],
"source": [
"p.run(20)"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"p.plot_population()"
]
},
{
"cell_type": "code",
"execution_count": 48,
"metadata": {},
"outputs": [],
"source": [
"t2 = Tree.from_serialization(t.serialize())"
]
},
{
"cell_type": "code",
"execution_count": 49,
"metadata": {},
"outputs": [],
"source": [
"t.mutate()"
]
},
{
"cell_type": "code",
"execution_count": 54,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0.00499001996007984"
]
},
"execution_count": 54,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"t.score()"
]
},
{
"cell_type": "code",
"execution_count": 51,
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"execution_count": 51,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"t.root().dot()"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'pepper'"
]
},
"execution_count": 32,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"list(t.root().childs())[0]._name"
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {},
"outputs": [],
"source": [
"n = IngredientNode(\"test\")"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 43,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"n.traverse() == IngredientNode"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}