diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40d66a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**/__pycache__ +**/.ipynb_checkpoints +**/local_tests** \ No newline at end of file diff --git a/NearestNeighbors/README.MD b/NearestNeighbors/README.MD new file mode 100644 index 0000000..9080ae4 --- /dev/null +++ b/NearestNeighbors/README.MD @@ -0,0 +1,133 @@ +# K Nearest Neighbors +## Introduction + +The use of maps to find nearest point of interest (e.g. closest restaurants) is common. However, using such features may reveals sensitive personal information. In this tutorial, we demonstrate how to use Zama's homomorphic encryption to find the K nearest restaurants to a given input while preserving the localisation's privacy. + +In this scenario, the server does not learn anything about a client's position at anytime as it sees only encrypted data. + +## Achieving Privacy +In our application demo: + + 1. The client owns its position. This information is never shared in clear with the server. + 1. The server owns map data. It only shares with the client the coordinates of the K closest restaurants to its position. + +To achieve this, FHE is used to implement swap sort. The server receives the encrypted coordinates and calculates the distance for each restaurant on the list. The server then performs a partial sort using swaps (an equivalent to [bubble sort](https://en.wikipedia.org/wiki/Bubble_sort)). It retrieves the encrypted coordinates of the K nearest restaurants and sends them to the client. The client decrypts the results. + +## Demo App +A demo streamlit app is available on [Quadratic's HuggingFace](https://huggingface.co/spaces/Quadratic-Labs/PrivateNearestNeighbors-FHE): + +It can also be deployed locally in the usual way: + + 1. Create a python virtual environnement and activate it. + 1. Install dependencies from requirements.txt: pip install -r requirements.txt + 1. Launch the app: streamlit run app.py + +The app depends also on files found in data/ folder: + + 1. circuit.zip : use generate_circuit.py to recompile the circuit if necessary (on new network data). + 1. key.zip : cached keys. If not present, they will be generated. + 1. restaurants.geojson: map data extracted from OSM (overpass-turbo API). See knn.ipynb for more information. + +## Data Extraction + +The data used as example in this notebook are extracted from [overpass-turbo](https://overpass-turbo.eu) using the following query : + +``` + // gather results + nwr["amenity"="restaurant"](48.874256063816,2.3389828205109,48.876909238658,2.3433548212051); + + // print results + out; +``` + +This query returns a list of restaurants in a given area + + +## Explanation of the swap sort + + +The swap sort is implemented using three TLUs : +``` + relu = fhe.univariate(lambda x: x if x > 0 else 0) + is_positive = fhe.univariate(lambda x: 1 if x > 0 else 0) + odd_halving = fhe.univariate(lambda x: (x - 1) // 2 if x % 2 else 0) +``` + +The first returns x if x is strictly positive and zero otherwise. +The second returns one if x is positive and zero otherwise. +The last returns half the value minus one for odd numbers, and zero otherwise. + + +The combination of the three functions is used to perform the swap. Here a snippet of the swap function: + +``` +def swap(this_idx, this_dist, that_idx, that_dist): + """ + Swaps this and that if this > that. + We must pass both the index and the distance for both this and that. + + Returns: + idxmin, min, idxmax, max of this and that based on distance + """ + diff = this_dist - that_dist + idx = odd_halving(2 * (this_idx - that_idx) + is_positive(diff)) + dist = relu(diff) + + idx_min = this_idx - idx + idx_max = that_idx + idx + dist_min = this_dist - dist + dist_max = that_dist + dist + return fhe.array([idx_min, dist_min, idx_max, dist_max]) +``` + +To decide weither or not to swap two values with their respective indices, we start by calculating the difference. + +``` +diff = this_dist - that_dist +``` + +Then, on the basis of this difference, we calculate 'idx', the index change to be made on the original indices. + + +``` +idx = odd_halving(2 * (this_idx - that_idx) + is_positive(diff)) +``` + +If the difference is positive (i.e. this > that), the input to odd_halving is always odd (2 * (this_idx - that_idx) is even). +The result of odd_halving is then (this_idx - that_idx), which is the difference to be applied to the original indices. Otherwise, the result is zero and the indices remain unchanged. + + +The values are swapped (or not) depending on the diff value. We use a relu to achieve this. If the difference is positive, the values should be swaped, otherwise the relu returns zero and the values remain unchanged. + + +The code extract below illustrates how the swap function works. + + +``` +if this > that : + diff = this_dist - that_dist > 0 + idx = odd_halving(2 * (this_idx - that_idx) + is_positive(diff)) = odd_halving(2 * (this_idx - that_idx) + 1) = this_idx - that_idx + dist = relu(diff) = diff + + idx_min = this_idx - idx = this_idx - (this_idx - that_idx) = that_idx + idx_max = that_idx + idx = that_idx + (this_idx - that_idx) = this_idx + dist_min = this_dist - dist = this_dist - diff = this_dist - (this_dist - that_dist) = that_dist + dist_max = that_dist + dist = that_dist + diff = that_dist + (this_dist - that_dist) = this_dist + +else : + diff = this_dist - that_dist < 0 + idx = odd_halving(2 * (this_idx - that_idx) + is_positive(diff)) = odd_halving(2 * (this_idx - that_idx) + 0) = 0 + dist = relu(diff) = 0 + + idx_min = this_idx - idx = this_idx + idx_max = that_idx + idx = that_idx + dist_min = this_dist - dist = this_dist + dist_max = that_dist + dist = that_dist +``` + +## Size of the map +The list of restaurants is presently confined to a predetermined area. We select seven restaurants and adjust the number of neighbors, K, to two (both parameters can be updated in config.py). With the current configuration on the Hugging Face server, the expected running times are up to 15 seconds for key generation and up to 4 minutes for the run. Increasing the number of displayed restaurants or the number of neighbors will significantly increase the execution time. Please refer to the figure below for different settings. + +![image](./figures/exec_time.png) + +One potential optimization is using [heap sort](https://en.wikipedia.org/wiki/Heapsort) instead of swap sort. This change will decrease the number of comparisons and, presumably, reduce the execution time. \ No newline at end of file diff --git a/NearestNeighbors/app.py b/NearestNeighbors/app.py new file mode 100644 index 0000000..52ec53a --- /dev/null +++ b/NearestNeighbors/app.py @@ -0,0 +1,140 @@ +import network +from shapely.geometry import Point +import streamlit as st +import config +from utils import ( + set_up_server, + set_up_client, + display_encrypted, + transform_point, + process_result, + display_map, + init_session, + add_to_server_side, + add_to_client_side, + display_client_side, + display_server_side, + restart_session, +) +from concrete import fhe + +server = set_up_server() +client = set_up_client(server.client_specs.serialize()) +restaurants, point_coordinates = network.get() + +c1, c2, c3 = init_session() + +with c1: + display_client_side() +with c2: + st_map = display_map(restaurants) +with c3: + display_server_side() + + +# keys generation view +if "evaluation_key" not in st.session_state: + with c1: + if st.button("Generate keys"): + with st.spinner("Generating keys"): + client.keys.load_if_exists_generate_and_save_otherwise( + config.keys_filepath + ) + st.session_state["evaluation_key"] = client.evaluation_keys.serialize() + add_to_client_side( + "Encryption/decryption keys and evaluation keys are generated." + ) + add_to_client_side("The evaluation key is sent to the server.") + add_to_server_side( + f"Evaluation key: {display_encrypted(st.session_state['evaluation_key'])}" + ) + st.rerun() + +else: + if "position" not in st.session_state: + with c1: + st.write("Select your starting position on the st_map") + + if st_map.get("last_clicked"): + position = Point( + st_map.get("last_clicked")["lng"], st_map.get("last_clicked")["lat"] + ) + add_to_client_side( + f"Selected starting point is: ({position.x}, {position.y})" + ) + st.session_state["position"] = position + st.rerun() + + if "position" in st.session_state and "encrypted_position" not in st.session_state: + position = st.session_state["position"] + with c1: + if st.button("Encrypt and send inputs"): + with st.spinner("Encrypting inputs"): + client.keys.load(config.keys_filepath) + + x, y = transform_point(position.x, position.y) + encrypted_x, encrypted_y = client.encrypt(x, y) + encrypted_position = ( + encrypted_x.serialize(), + encrypted_y.serialize(), + ) + add_to_server_side( + f"Received starting point: ({display_encrypted(encrypted_position[0])},{display_encrypted(encrypted_position[1])})" + ) + st.session_state["encrypted_position"] = encrypted_position + st.rerun() + + if ( + "encrypted_position" in st.session_state + and "encrypted_results" not in st.session_state + ): + with c3: + encrypted_x = st.session_state["encrypted_position"][0] + encrypted_y = st.session_state["encrypted_position"][1] + + if st.button(f"Find {config.number_of_neighbors} nearest restaurants"): + with st.spinner("Computing"): + st.write("Please wait, it may take few minutes.") + deserialized_x = fhe.Value.deserialize(encrypted_x) + deserialized_y = fhe.Value.deserialize(encrypted_y) + deserialized_evaluation_keys = fhe.EvaluationKeys.deserialize( + st.session_state["evaluation_key"] + ) + client.keys.load_if_exists_generate_and_save_otherwise( + config.keys_filepath + ) + res = server.run( + deserialized_x, + deserialized_y, + evaluation_keys=deserialized_evaluation_keys, + ) + ser_res = fhe.Value.serialize(res) + add_to_server_side( + f"The encrypted result {display_encrypted(ser_res)} is sent to the client." + ) + add_to_client_side( + f"The received encrypted result is: {display_encrypted(ser_res)}." + ) + st.session_state["encrypted_results"] = ser_res + st.rerun() + + if ( + "encrypted_results" in st.session_state + and "decrypted_result" not in st.session_state + ): + res = st.session_state["encrypted_results"] + with c1: + if st.button("Decrypt result"): + with st.spinner("Computing"): + client.keys.load_if_exists_generate_and_save_otherwise( + config.keys_filepath + ) + deser_res = fhe.Value.deserialize(res) + decrypted_result = client.decrypt(deser_res) + process_result(restaurants, decrypted_result) + st.session_state["decrypted_result"] = decrypted_result + st.rerun() + + if "decrypted_result" in st.session_state: + with c1: + restart_session() diff --git a/NearestNeighbors/comparison/KnnConcrete.ipynb b/NearestNeighbors/comparison/KnnConcrete.ipynb new file mode 100644 index 0000000..59fea13 --- /dev/null +++ b/NearestNeighbors/comparison/KnnConcrete.ipynb @@ -0,0 +1,208 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/riad/envs/zama/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, + { + "data": { + "text/html": [ + "
KNeighborsClassifier(n_bits=6)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "KNeighborsClassifier(n_bits=6)" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import time\n", + "\n", + "from concrete.ml.sklearn import KNeighborsClassifier as ConcreteKNeighborsClassifier\n", + "import numpy as np\n", + "points_array = np.array([\n", + " [2, 3], [1, 5], [3, 2], [5, 2], [1, 1],\n", + " [9, 4], [13, 2], [14, 13], [9, 8], [8, 0],\n", + " [2, 10], [3, 8], [8, 12], [4, 10], [7, 7],\n", + "])\n", + "N_PTS = points_array.shape[0]\n", + "\n", + "n_neighbors = 3\n", + "concrete_knn = ConcreteKNeighborsClassifier(n_bits=6, n_neighbors=n_neighbors)\n", + "indexes = np.arange(N_PTS)\n", + "concrete_knn.fit(points_array.astype(float), indexes)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Compilation time: 16.67 seconds\n" + ] + } + ], + "source": [ + "time_begin = time.time()\n", + "circuit = concrete_knn.compile(points_array)\n", + "print(f\"Compilation time: {time.time() - time_begin:.2f} seconds\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Maximum bit-width reached in the circuit: 14\n" + ] + } + ], + "source": [ + "print(f\"Maximum bit-width reached in the circuit: {circuit.graph.maximum_integer_bit_width()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Key generation time: 3.44 seconds\n" + ] + } + ], + "source": [ + "time_begin = time.time()\n", + "circuit.client.keygen()\n", + "print(f\"Key generation time: {time.time() - time_begin:.2f} seconds\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "x, y = 4, 3" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "FHE inference execution time:--- 1203.0613384246826 seconds ---\n" + ] + } + ], + "source": [ + "start_time = time.time()\n", + "decrypted_res = concrete_knn.get_topk_labels([(x,y)], fhe=\"execute\")\n", + "print(\"FHE inference execution time:--- %s seconds ---\" % (time.time() - start_time))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Gives the same execution time but inacurate results\n", + "# start_time = time.time()\n", + "# pos_enc = circuit.client.encrypt([[x, y]])\n", + "# result = circuit.server.run(pos_enc, evaluation_keys=circuit.client.evaluation_keys)\n", + "# decrypted_res = circuit.client.decrypt(result)\n", + "# print(\"FHE inference execution time:--- %s seconds ---\" % (time.time() - start_time))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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lLW/33YdZEoCgwwREZH8WPv7bdeHloiIiIgKwyBDREREssUgQ0RERLLFIENERESyxSBDREREssUgQ0RERLLFIENERESyxSBDREREssUgQ0RERLLFIENERESyxSBDREREssUgQ0RERLLFIENERESyxSBDREREslXkIJOYmIhffvnF+Pzjjz9Go0aN8N///hc3btwo0eKIqGzS6gQOnb+OzSmXcej8dWh1QuqSiEimihxkJk+ejJycHADAyZMn8dprr6Fz585IT0/HpEmTSrxAIipbElPVaDF3D/qt+A3j16Wg34rf0GLuHiSmqqUujYhkqMhBJj09HWFhYQCA7777Di+88ALee+89fPzxx9i2bVuJF0hEZUdiqhqxa5Kh1uSbtGdq8hG7JplhhoiKrMhBxtnZGXl5eQCAXbt2oX379gAAb29v454aIqJHaXUC8VvSYOkgkqEtfksaDzMRUZE4FnWGFi1aYNKkSYiKikJSUhK++eYbAMDZs2cRFBRU4gUSUdmQlJ5ttifmYQKAWpOPpPRsNAv1sV1hRCRrRd4j89FHH8HR0REbNmxAQkICqlatCgDYtm0bOnbsWOIFElHZkJVbcIgpznREREAx9shUq1YNW7duNWtfuHBhiRRERGWTn4driU5HRARYGWRycnLg6elp/P/CGKYjInpYZIg3AlSuyNTkWxwnowDgr3JFZIi3rUsjIhmzKshUqlQJarUafn5+8PLygkKhMJtGCAGFQgGtVlviRRKR/CkdFIiLCUPsmmQoAJMwY9iixMWEQelgvn0hIiqIVUFmz5498Pb2Nv6/pSBDRPQ4HcMDkDCgMeK3pJkM/PVXuSIuJgwdwwMkrI6I5EghhCjT5zrm5ORApVJBo9HwsBeRndDqBJLSs5GVmw8/D/3hJO6JIaKHWfv7XeSzlmbOnAmdTmfWrtFo0K9fv6IujojKIaWDAs1CfdCtUVU0C/VhiCGiYitykPnss8/QokULXLhwwdi2b98+RERE4Pz58yVaHBEREVFhihxkfv/9dwQFBaFRo0ZYsWIFJk+ejPbt22PgwIE4ePBgadRIREREZFGRryNTqVIlfPvtt3jzzTcxcuRIODo6Ytu2bWjXrl1p1EdERERUoCLvkQGApUuXYvHixejXrx9q1qyJcePG4cSJEyVdGxEREVGhihxkOnbsiPj4eKxevRpr167F8ePH8dxzz+E///kP5s2bVxo1EhEREVlU5CCj1Wrx+++/o2fPngAANzc3JCQkYMOGDbxNAREREdlUiV5H5p9//kHlypVLanElgteRISIikp9Su45MYewtxBAREVHZVuSzlrRaLRYuXIhvv/0Wf/31F+7du2fyenZ2dokVR0RERFSYIu+RiY+Px4cffog+ffpAo9Fg0qRJ6NGjBxwcHDBz5sxSKJGIiIjIsiIHmbVr12LFihV47bXX4OjoiH79+uF///sfZsyYgd9++600aiQiIiKyqMhBJjMzExEREQCAihUrQqPRAABeeOEF/PjjjyVbHREREVEhihxkgoKCoFarAQChoaHYsWMHAODIkSNwcXEp2eqIiIiIClHkIPPiiy9i9+7dAICxY8di+vTpqF27NgYNGoRhw4aVeIFEREREBXni68gcOnQIhw4dQu3atRETE1NSdZUYXkeGiIhIfmx2HZlmzZph0qRJpRZiLl++jAEDBsDHxwdubm6IiIjA0aNHS+W9iIiISF6eKMh4enriwoULJVWLmRs3biAqKgpOTk7Ytm0b0tLS8MEHH6BSpUql9p5EREQkH1ZfEO/KlSsIDAw0aSvBuxtYNHfuXAQHB2PlypXGtpCQkFJ9TyIiIpIPq/fI1K9fH1999VVp1mLmhx9+QNOmTdGrVy/4+fnh6aefxooVKwqd5+7du8jJyTF5EBERUdlkdZCZPXs2Ro4ciV69ehlvQzBgwIBSHUB74cIFJCQkoHbt2ti+fTtiY2Mxbtw4rF69usB55syZA5VKZXwEBweXWn1EREQkrSKdtZSeno7hw4cjLS0NK1asKPWzlJydndG0aVMcPHjQ2DZu3DgcOXIEhw4dsjjP3bt3cffuXePznJwcBAcH86wlIiIiGbH2rKUi3TQyJCQEe/bswUcffYQePXqgXr16cHQ0XURycnLxKrYgICAAYWFhJm316tXDd999V+A8Li4uvDAfERFROVHku19nZGRg48aNqFSpErp162YWZEpSVFQUzpw5Y9J29uxZVK9evdTek4iIiOSjSCnEcLPI6Oho/PHHH/D19S2tugAAEydORPPmzfHee++hd+/eSEpKwvLly7F8+fJSfV8iIiKSB6vHyHTs2BFJSUlYtGgRBg0aVNp1GW3duhXTpk3DuXPnEBISgkmTJuGVV16xen5e2ZeIiEh+SnyMjFarxe+//46goKASKdBaL7zwAl544QWbvicRERHJg9VBZufOnaVZBxEREVGRPfG9loiIiIikwiBDREREssUgQ0RERLLFIENERESyxSBDREREssUgQ0RERLLFIENERESyxSBDREREssUgQ0RERLLFIENERESyxSBDREREssUgQ0RERLLFIENERESyZfXdr+lfWp1AUno2snLz4efhisgQbygdFFKXRUREVO4wyBRRYqoa8VvSoNbkG9sCVK6IiwlDx/AACSsjIiIqf3hoqQgSU9WIXZNsEmIAIFOTj9g1yUhMVUtUGRERUfnEIGMlrU4gfksahIXXDG3xW9Kg1VmagoiIiEoDg4yVktKzzfbEPEwAUGvykZSebbuiiIiIyjkGGStl5RYcYoozHRERET05Bhkr+Xm4luh0RERE9OQYZKwUGeKNAJUrCjrJWgH92UuRId62LIuIiKhcY5CxktJBgbiYMAAwCzOG53ExYbyeDBERkQ0xyBRBx/AAJAxoDH+V6eEjf5UrEgY05nVkiIiIbIwXxCuijuEBeD7Mn1f2JSIisgMMMsWgdFCgWaiP1GUQERGVezy0RERERLLFIENERESyxSBDREREssUgQ0RERLLFIENERESyxSBDREREssUgQ0RERLLFIENERESyxSBDREREssUgQ0RERLLFIENERESyxSBDREREssUgQ0RERLLFIENERESyJasg8/7770OhUGDChAlSl0JEVKK0OoFD569jc8plHDp/HVqdkLokIllwlLoAax05cgSffvopGjRoIHUpREQlKjFVjfgtaVBr8o1tASpXxMWEoWN4gISVEdk/WeyRuXXrFvr3748VK1agUqVKUpdDRFRiElPViF2TbBJiACBTk4/YNclITFVLVBmRPMgiyIwePRpdunRBdHS01KUQEZUYrU4gfksaLB1EMrTFb0njYSaiQtj9oaV169YhOTkZR44csWr6u3fv4u7du8bnOTk5pVUaEdETSUrPNtsT8zABQK3JR1J6NpqF+tiuMCIZses9MpcuXcL48eOxdu1auLq6WjXPnDlzoFKpjI/g4OBSrpKIqHiycgsOMcWZjqg8susgc+zYMWRlZaFx48ZwdHSEo6Mj9u/fjyVLlsDR0RFardZsnmnTpkGj0Rgfly5dkqByIqLH8/Ow7g80a6cjKo/s+tBSu3btcPLkSZO2oUOHom7dupgy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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "\n", + "xs = [point[0] for point in points_array]\n", + "ys = [point[1] for point in points_array]\n", + "\n", + "\n", + "plt.scatter(xs, ys)\n", + "\n", + "\n", + "plt.scatter(x, y, color='red', label='Input')\n", + "\n", + "for point in decrypted_res[0]:\n", + " plt.scatter(xs[point], ys[point], marker='o', facecolor='none', edgecolor='red', s=200)\n", + "\n", + "\n", + "plt.xlabel('X-axis')\n", + "plt.ylabel('Y-axis')\n", + "plt.title('Input point (colored) and its closest neighbors (circled)')\n", + "plt.legend()\n", + "\n", + "plt.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "zama", + "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.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/NearestNeighbors/comparison/KnnQuadratic.ipynb b/NearestNeighbors/comparison/KnnQuadratic.ipynb new file mode 100644 index 0000000..34c3b84 --- /dev/null +++ b/NearestNeighbors/comparison/KnnQuadratic.ipynb @@ -0,0 +1,240 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/riad/envs/zama/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "from concrete import fhe\n", + "import time\n", + "import numpy as np\n", + "points_array = np.array([\n", + " [2, 3], [1, 5], [3, 2], [5, 2], [1, 1],\n", + " [9, 4], [13, 2], [14, 13], [9, 8], [8, 0],\n", + " [2, 10], [3, 8], [8, 12], [4, 10], [7, 7],\n", + "])\n", + "N_PTS = points_array.shape[0]\n", + "points = fhe.LookupTable(points_array.flatten())\n", + "n_neighbors = 3\n", + "\n", + "def get_point(index):\n", + " return (points[2*index], points[2*index + 1])\n", + "\n", + "\n", + "def all_distances(x, y):\n", + " xs = np.arange(0, 2 * N_PTS, 2)\n", + " ys = np.arange(1, 2 * N_PTS, 2)\n", + " a = abs(points[xs] - x)\n", + " b = abs(points[ys] - y)\n", + " return a + b\n", + "# TLUs\n", + "relu = fhe.univariate(lambda x: x if x > 0 else 0)\n", + "is_positive = fhe.univariate(lambda x: 1 if x > 0 else 0)\n", + "odd_halving = fhe.univariate(lambda x: (x-1)//2 if x % 2 else 0) \n", + "\n", + "def swap(this_idx, this_dist, that_idx, that_dist):\n", + " \"\"\"\n", + " Swaps this and that if this > that. \n", + " We must pass both the index and the distance for both this and that.\n", + "\n", + " Returns:\n", + " idxmin, min, idxmax, max of this and that based on distance\n", + " \"\"\"\n", + " diff = this_dist - that_dist\n", + " idx = odd_halving((this_idx - that_idx) + (this_idx - that_idx) + is_positive(diff))\n", + " dist = relu(diff)\n", + "\n", + " idx_min = this_idx - idx\n", + " idx_max = that_idx + idx \n", + " dist_min = this_dist - dist\n", + " dist_max = that_dist + dist\n", + " return fhe.array([idx_min, dist_min, idx_max, dist_max])\n", + "\n", + "\n", + "@fhe.compiler({\"x\": \"encrypted\", \"y\": \"encrypted\"})\n", + "def knn(x, y):\n", + " dist = all_distances(x, y)\n", + " idx = list(range(N_PTS))\n", + " for k in range(n_neighbors):\n", + " for i in range(k+1, N_PTS):\n", + " idx[k], dist[k], idx[i], dist[i] = swap(idx[k], dist[k], idx[i], dist[i])\n", + " return fhe.array([get_point(idx[j]) for j in range(n_neighbors)])\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Compilation time: 7.41 seconds\n" + ] + } + ], + "source": [ + "inputset = [(4, 3), (0, 0), (15, 3), (4, 15), (9, 4), (13, 2), (14, 13), (9, 8), (8, 0), (2, 10), (3, 8), (8, 12), (4, 10), (7, 7)]\n", + "time_begin = time.time()\n", + "circuit = knn.compile(inputset)\n", + "print(f\"Compilation time: {time.time() - time_begin:.2f} seconds\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Maximum bit-width reached in the circuit: 6\n" + ] + } + ], + "source": [ + "print(f\"Maximum bit-width reached in the circuit: {circuit.graph.maximum_integer_bit_width()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Key generation time: 8.71 seconds\n" + ] + } + ], + "source": [ + "time_begin = time.time()\n", + "circuit.client.keygen()\n", + "print(f\"Key generation time: {time.time() - time_begin:.2f} seconds\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "x, y = 4, 3" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "ex, ey = circuit.encrypt(x, y)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- 17.16662859916687 seconds ---\n" + ] + } + ], + "source": [ + "start_time = time.time()\n", + "result = circuit.server.run(ex, ey, evaluation_keys=circuit.client.evaluation_keys)\n", + "print(\"--- %s seconds ---\" % (time.time() - start_time))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "decrypted_res = circuit.decrypt(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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tBXk1y+pqal5bnv37dsndeklpqB15VnWPP1aIUQZuowlERERlSoc7EtERESyxSBDREREssUgQ0RERLLFIENERESyxSBDREREssUgQ0RERLLFIENERESyxSBDRGXCzz//DIVCgTt37khdChEVIwYZIrIqjUaDJk2aoEuXLkbtarUagYGB+OCDD0rkfZs0aYL09HSoVKoSWT4RSYNX9iUiqzt37hzq16+PpUuXonfv3gCAfv364eTJkzhy5EipvV8NERU/7pEhIqurWbMmPv30U4wcORLp6enYvHkz1qxZg2+//TbPEDNhwgTUrFkT5cqVQ/Xq1TF58mQ8fvwYgO5O8K1bt0bbtm2h/9ssKysLAQEBmDJlCgDTQ0tpaWmIjo6Gh4cHXF1dUadOHWzbtq3kPzwRFSve/ZqIJDFy5Ehs3LgRffv2xalTpzBlyhTUq1cvz+nd3NywfPly+Pv749SpU3j77bfh5uaG9957DwqFAitWrEBYWBgWLFiA0aNHY9iwYahcubIhyDxr+PDhePToEQ4cOABXV1ekpKSgfPnyJfVxiaiE8NASEUnmzJkzqF27NsLCwpCUlAR7e8v/tpozZw7WrFmDo0ePGtrWrVuHfv36YcyYMVi4cCGOHz+OkJAQALo9Mi1atMDt27dRoUIFvPjii3jzzTcRGxtb7J+LiKyHh5aISDLffPMNypUrh9TUVPz1118AgGHDhqF8+fKGh97atWsREREBX19flC9fHh9++CGuXLlitLxu3brhjTfewKeffoo5c+YYQow5o0aNwscff4yIiAjExsbijz/+KJkPSUQlikGGiCRx6NAhzJ07F1u3bkV4eDgGDx4MIQQ++ugjnDhxwvAAgMOHD6N3795o3749tm7diuPHj+ODDz7Ao0ePjJb54MEDHDt2DEqlEufPn8/3/d966y1cunTJcGirUaNGWLhwYUl9XCIqIQwyRGR1Dx48wIABAxATE4MWLVrg66+/RmJiIhYvXgwfHx/UqFHD8AB0oadq1ar44IMP0KhRI4SEhCAtLc1kue+++y7s7Oywfft2LFiwAHv37s23jsDAQAwbNgwbNmzAu+++i6VLl5bI5yWiksMgQ0RWN2nSJAgh8OmnnwIAqlWrhjlz5uC9997D5cuXTaYPCQnBlStXsGbNGly8eBELFizAxo0bjab56aef8M0332DVqlV47bXXMH78ePTv3x+3b982W8OYMWOwY8cOpKamIikpCfv27UPt2rWL/bMSUcniYF8isqr9+/ejVatW+PnnnxEZGWn0Wtu2bfHkyRPs3r0bCoXC6LX33nsP33zzDXJzc9GhQwf861//wtSpU3Hnzh3cvHkTYWFhGD16NCZNmgQAePz4MRo3bozg4GCsXbvWZLDvyJEjsX37dvz1119wd3dHVFQU5s6dCy8vL6v1BRE9PwYZIiIiki0eWiIiIiLZYpAhIiIi2WKQISIiItlikCEiIiLZYpAhIiIi2WKQISIiItlikCEiIiLZYpAhIiIi2WKQISIiItlikCEiIiLZYpAhIiIi2WKQISIiItn6P+RmVQmXFqQGAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "\n", + "xs = [point[0] for point in points_array]\n", + "ys = [point[1] for point in points_array]\n", + "\n", + "\n", + "plt.scatter(xs, ys)\n", + "\n", + "\n", + "plt.scatter(x, y, color='red', label='First location')\n", + "\n", + "for point in decrypted_res:\n", + " plt.scatter(point[0], point[1], marker='o', facecolor='none', edgecolor='red', s=200)\n", + "\n", + "\n", + "plt.xlabel('X-axis')\n", + "plt.ylabel('Y-axis')\n", + "plt.title('Input point (colored) and its closest neighbors (circled)')\n", + "plt.legend()\n", + "\n", + "plt.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "zama", + "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.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/NearestNeighbors/config.py b/NearestNeighbors/config.py new file mode 100644 index 0000000..4b8cac9 --- /dev/null +++ b/NearestNeighbors/config.py @@ -0,0 +1,8 @@ +from pathlib import Path + +data_path = Path(__file__).parent / "data" +restaurants_filepath = data_path / "restaurants.geojson" +circuit_filepath = data_path / "circuit.zip" +keys_filepath = data_path / "keys.zip" +total_restaurants_number = 7 +number_of_neighbors = 2 diff --git a/NearestNeighbors/data/restaurants.geojson b/NearestNeighbors/data/restaurants.geojson new file mode 100644 index 0000000..449d456 --- /dev/null +++ b/NearestNeighbors/data/restaurants.geojson @@ -0,0 +1,4618 @@ +{ + "type": "FeatureCollection", + "generator": "overpass-ide", + "copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.", + "timestamp": "2023-09-25T15:05:05Z", + "features": [ + { + "type": "Feature", + "properties": { + "@id": "node/287612106", + "amenity": "restaurant", + "name": "Vadrouille" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3373832, + 48.8773406 + ] + }, + "id": "node/287612106" + }, + { + "type": "Feature", + "properties": { + "@id": "node/428141439", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "22", + "contact:phone": "+33 1 48 00 07 73", + "contact:postcode": "75009", + "contact:street": "Rue de Trévise", + "cuisine": "chinese", + "description": "Spécialités du Hunan", + "indoor_seating": "yes", + "name": "L'Orient d'Or", + "name:fr": "L'Orient d'Or", + "name:zh": "福源丰", + "opening_hours": "Tu-Su 12:00-14:30,19:00-22:30; Mo off", + "outdoor_seating": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3454201, + 48.8739244 + ] + }, + "id": "node/428141439" + }, + { + "type": "Feature", + "properties": { + "@id": "node/663314810", + "amenity": "restaurant", + "name": "La Rimaudière", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3394255, + 48.8768016 + ] + }, + "id": "node/663314810" + }, + { + "type": "Feature", + "properties": { + "@id": "node/971476013", + "amenity": "restaurant", + "name": "Les Ailes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3443375, + 48.8739835 + ] + }, + "id": "node/971476013" + }, + { + "type": "Feature", + "properties": { + "@id": "node/971476045", + "amenity": "restaurant", + "cuisine": "chinese", + "name": "Hong Kong" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3456817, + 48.876213 + ] + }, + "id": "node/971476045" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1232772691", + "addr:city": "Paris", + "addr:housenumber": "46", + "addr:postcode": "75009", + "addr:street": "Rue La Fayette", + "amenity": "restaurant", + "contact:phone": "+33 1 73 77 82 66", + "contact:website": "https://www.kozy.fr/kozy-kanope-brunch", + "cuisine": "cake;coffee_shop;french;juice;pancake;brunch", + "delivery": "yes", + "diet:vegetarian": "yes", + "kids_area": "no", + "level": "0", + "microbrewery": "no", + "name": "Kozy Kanope", + "opening_hours": "Mo-Fr 08:00-15:00; Sa-Su 09:30-16:00", + "outdoor_seating": "no", + "reservation": "no", + "self_service": "no", + "smoking": "no", + "takeaway": "yes", + "toilets": "yes", + "toilets:access": "customers", + "toilets:wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3401877, + 48.8747844 + ] + }, + "id": "node/1232772691" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1236456908", + "addr:housenumber": "23", + "addr:postcode": "75009", + "addr:street": "Rue Lamartine", + "amenity": "restaurant", + "contact:instagram": "piknik_paris", + "cuisine": "asian", + "name": "Piknik", + "name:ko": "피크닉", + "opening_hours": "Mo-Fr 12:00-14:30", + "source": "cadastre-dgi-fr source : Direction Générale des Impôts - Cadastre. Mise à jour : 2011" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3415422, + 48.8764927 + ] + }, + "id": "node/1236456908" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1242438647", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "35", + "contact:postcode": "75009", + "contact:street": "Rue de Trévise", + "cuisine": "mexican", + "name": "Black bean", + "name:fr": "Black beans Mexicain", + "source": "cadastre-dgi-fr source : Direction Générale des Impôts - Cadastre. Mise à jour : 2011" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.345311, + 48.8748766 + ] + }, + "id": "node/1242438647" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1242439149", + "addr:housenumber": "58", + "addr:street": "Rue La Fayette", + "amenity": "restaurant", + "name": "L'Atelier Saisonnier", + "source": "cadastre-dgi-fr source : Direction Générale des Impôts - Cadastre. Mise à jour : 2011" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3424351, + 48.8753894 + ] + }, + "id": "node/1242439149" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1242504317", + "addr:housenumber": "16", + "amenity": "restaurant", + "name": "A la", + "name:fr": "Grange beteliere" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3412456, + 48.8732347 + ] + }, + "id": "node/1242504317" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1242504439", + "addr:housenumber": "12", + "addr:postcode": "75009", + "addr:street": "Rue Buffault", + "amenity": "restaurant", + "cuisine": "sandwich", + "internet_access": "wlan", + "name": "Les Bariolés de Maud", + "opening_hours": "Mo-Fr 11:00-15:00; Sa,Su 10:00-16:00", + "phone": "+33 9 73 17 01 68", + "source": "cadastre-dgi-fr source : Direction Générale des Impôts - Cadastre. Mise à jour : 2011", + "website": "http://lesbariolesdemaud.fr" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3419536, + 48.8755706 + ] + }, + "id": "node/1242504439" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1293504445", + "addr:housenumber": "77", + "addr:postcode": "75009", + "addr:street": "Rue Taitbout", + "amenity": "restaurant", + "changing_table": "no", + "cuisine": "asian;korean;japanese", + "indoor_seating": "yes", + "level": "0", + "name": "Dolsotbat", + "opening_hours": "Mo-Sa 12:00-14:30, 19:00-22:30", + "outdoor_seating": "no", + "phone": "+33 1 48 78 47 44", + "smoking": "no", + "source": "survey", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes", + "toilets": "yes", + "toilets:access": "customers", + "toilets:disposal": "flush", + "toilets:position": "seated", + "toilets:wheelchair": "no", + "wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3352931, + 48.8765206 + ] + }, + "id": "node/1293504445" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1293504454", + "amenity": "restaurant", + "contact:housenumber": "43", + "contact:street": "Rue Saint-Lazare", + "cuisine": "portuguese;petiscos;bacalhau", + "diet:vegetarian": "no", + "indoor_seating": "yes", + "name": "CDP Paris Lazare", + "opening_hours": "Mo-Sa 12:00-00:00", + "outdoor_seating": "yes", + "phone": "+33 1 48 74 32 94", + "short_name": "CDP Lazare", + "strapline": "Cuisine Lusitanienne by Alfredo Martins", + "website": "https://cdp-paris.fr/fr/restaurants-portugais-de-qualite-by-alfredo-martins", + "wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3353158, + 48.876753 + ] + }, + "id": "node/1293504454" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1293504460", + "addr:housenumber": "75", + "addr:street": "Rue Taitbout", + "amenity": "restaurant", + "cuisine": "pasta", + "description": "Small pasta-oriented restaurant / take-away", + "name": "Pasta & Co" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3352844, + 48.8764639 + ] + }, + "id": "node/1293504460" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1293504477", + "addr:city": "Paris", + "addr:housenumber": "75", + "addr:postcode": "75009", + "addr:street": "Rue Taitbout", + "amenity": "restaurant", + "cuisine": "asian", + "name": "Angkor Maison", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3352774, + 48.8764321 + ] + }, + "id": "node/1293504477" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1294579480", + "amenity": "restaurant", + "cuisine": "regional", + "name": "Café Lorette", + "outdoor_seating": "yes", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3383718, + 48.8760923 + ] + }, + "id": "node/1294579480" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1294579482", + "addr:postcode": "75009", + "addr:street": "Rue La Fayette", + "amenity": "restaurant", + "changing_table": "no", + "cuisine": "chinese", + "level": "0", + "microbrewery": "no", + "name": "Sucrépice", + "opening_hours": "Tu-Su 12:00-22:30", + "outdoor_seating": "no", + "self_service": "no", + "smoking": "no", + "stars": "0", + "toilets": "yes", + "toilets:access": "customers", + "toilets:disposal": "flush", + "toilets:position": "seated", + "toilets:wheelchair": "no", + "wheelchair": "limited" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3383294, + 48.8745269 + ] + }, + "id": "node/1294579482" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1294579501", + "amenity": "restaurant", + "cuisine": "french", + "name": "Le Laffitte", + "phone": "+33 1 42 80 07 66", + "smoking": "no", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.338336, + 48.8751689 + ] + }, + "id": "node/1294579501" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1440250986", + "amenity": "restaurant", + "cuisine": "french", + "name": "Chez Vous", + "smoking": "separated", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3398701, + 48.8776662 + ] + }, + "id": "node/1440250986" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1445872328", + "amenity": "restaurant", + "cuisine": "french", + "name": "Les Demoiselles de Lorette", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3370142, + 48.8764828 + ] + }, + "id": "node/1445872328" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1445872334", + "amenity": "restaurant", + "cuisine": "italian", + "name": "Salsamenteria di Parma", + "source": "survey 2017" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3371884, + 48.8765216 + ] + }, + "id": "node/1445872334" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1482958090", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "34", + "contact:postcode": "75009", + "contact:street": "Rue de la Victoire", + "contact:website": "https://www.memento-paris.fr", + "cuisine": "traditional;french", + "name": "Memento", + "opening_hours": "Mo-Fr 12:00-14:30", + "smoking": "no", + "source": "survey", + "takeaway": "yes", + "wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3376319, + 48.875193 + ] + }, + "id": "node/1482958090" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1482958091", + "amenity": "restaurant", + "cuisine": "regional", + "name": "Le Chantereine", + "phone": "+33 1 48 74 49 86", + "source": "survey", + "wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3355442, + 48.8751468 + ] + }, + "id": "node/1482958091" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1482958093", + "amenity": "restaurant", + "cuisine": "chinese", + "fax": "+33 9 50 55 66 68", + "internet_access": "wlan", + "name": "Paradis des Pâtes", + "opening_hours": "Mo-Sa 11:00-23:00", + "smoking": "no", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3392304, + 48.8750364 + ] + }, + "id": "node/1482958093" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1483017712", + "addr:city": "Paris", + "addr:housenumber": "15", + "addr:postcode": "75009", + "addr:street": "Rue Saint-Lazare", + "amenity": "restaurant", + "cuisine": "korean", + "name": "Bib!mbar", + "source": "survey", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3373282, + 48.8766742 + ] + }, + "id": "node/1483017712" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1483017713", + "addr:city": "Paris", + "addr:housenumber": "21", + "addr:postcode": "75009", + "addr:street": "Rue Saint-Lazare", + "amenity": "restaurant", + "capacity": "60", + "cuisine": "french;bistro", + "email": "bonjour@mieux-restaurant.com", + "name": "Mieux", + "opening_hours": "Mo-Sa 12:00-14:30,19:30-22:30", + "phone": "+33 1 71 32 46 73", + "smoking": "no", + "takeaway": "no", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes", + "website": "http://mieux-restaurant.com/", + "wheelchair": "limited" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3368624, + 48.8766497 + ] + }, + "id": "node/1483017713" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1483017719", + "amenity": "restaurant", + "cuisine": "french", + "name": "Jean", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3383704, + 48.8769295 + ] + }, + "id": "node/1483017719" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1483017723", + "amenity": "restaurant", + "cuisine": "argentinian", + "name": "Pony Polo", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3369941, + 48.8767991 + ] + }, + "id": "node/1483017723" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1483017727", + "amenity": "restaurant", + "cuisine": "indian", + "name": "Mamtajmahal", + "smoking": "no", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.336497, + 48.8768072 + ] + }, + "id": "node/1483017727" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1483017732", + "amenity": "restaurant", + "brand": "Bistro Régent", + "cuisine": "steak_house;french", + "name": "Bistro Régent", + "smoking": "no", + "source": "survey", + "wheelchair": "limited" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3388379, + 48.875855 + ] + }, + "id": "node/1483017732" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1483017733", + "amenity": "restaurant", + "cuisine": "japanese", + "name": "Yokhama", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3367754, + 48.8767897 + ] + }, + "id": "node/1483017733" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1483017735", + "amenity": "restaurant", + "cuisine": "japanese", + "name": "Sakura", + "opening_hours": "Mo-Sa 11:30-15:00,18:30-22:30; Su 18:30-22:30", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3379529, + 48.8767676 + ] + }, + "id": "node/1483017735" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1489467283", + "amenity": "restaurant", + "name": "Gontran", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3393542, + 48.8749956 + ] + }, + "id": "node/1489467283" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1675451555", + "amenity": "restaurant", + "check_date": "2023-08-06", + "contact:city": "Paris", + "contact:housenumber": "25", + "contact:postcode": "75009", + "contact:street": "Rue Le Peletier", + "contact:website": "https://www.restaurant-aupetitriche.com/", + "cuisine": "french", + "name": "Au Petit Riche" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.33857, + 48.8731527 + ] + }, + "id": "node/1675451555" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1677167520", + "amenity": "restaurant", + "cuisine": "pizza", + "level": "0", + "name": "Pizza Capri", + "outdoor_seating": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.343092, + 48.8739879 + ] + }, + "id": "node/1677167520" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1824826034", + "amenity": "restaurant", + "cuisine": "french", + "name": "Saveurs et Coïncedences" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3451791, + 48.8728885 + ] + }, + "id": "node/1824826034" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1827138878", + "amenity": "restaurant", + "cuisine": "chinese", + "name": "Bao nan", + "smoking": "no", + "wheelchair": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3394762, + 48.8765391 + ] + }, + "id": "node/1827138878" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1827138887", + "amenity": "restaurant", + "cuisine": "japanese", + "name": "Kamado", + "phone": "+33 1 42 80 39 92" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3400961, + 48.8762148 + ] + }, + "id": "node/1827138887" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1827138888", + "amenity": "restaurant", + "cuisine": "asian", + "name": "Magokoro", + "smoking": "no", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3406889, + 48.8765114 + ] + }, + "id": "node/1827138888" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1827138894", + "amenity": "restaurant", + "cuisine": "regional", + "email": "info@restaurant-les-saisons.com", + "name": "Les Saisons", + "opening_hours": "12:00-14:30,19:15-22:45", + "phone": "+33 1 40 16 08 00", + "website": "https://www.restaurant-les-saisons.com/", + "wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3401427, + 48.8766574 + ] + }, + "id": "node/1827138894" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1827138908", + "amenity": "restaurant", + "cuisine": "korean", + "name": "Sambuja", + "phone": "+33 1 53 21 07 89" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3395753, + 48.876475 + ] + }, + "id": "node/1827138908" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1827138910", + "addr:housenumber": "44", + "addr:street": "Rue Lamartine", + "amenity": "restaurant", + "cuisine": "chinese", + "name": "Xi'an", + "opening_hours": "Mo-Sa 11:45-14:45, 18:30-22:30", + "phone": "+33 1 42 81 38 07", + "smoking": "outside", + "wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3408729, + 48.8766336 + ] + }, + "id": "node/1827138910" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1827138913", + "amenity": "restaurant", + "cuisine": "french", + "name": "Le Milton", + "opening_hours": "Mo-Sa 10:00-19:00", + "operator": "Mme Epale Sophie", + "phone": "+33 1 45 26 62 20", + "smoking": "outside", + "website": "https://fr-fr.facebook.com/Les-madeleines-parisiennes-343278205695571/", + "wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3405656, + 48.8766574 + ] + }, + "id": "node/1827138913" + }, + { + "type": "Feature", + "properties": { + "@id": "node/1980664953", + "amenity": "restaurant", + "name": "La Boule Rouge" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3445761, + 48.8731487 + ] + }, + "id": "node/1980664953" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2103216535", + "amenity": "restaurant", + "name": "Chez Yanina", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3429609, + 48.8727963 + ] + }, + "id": "node/2103216535" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2103216538", + "amenity": "restaurant", + "name": "Ravioli Nord-Est", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3429812, + 48.8727029 + ] + }, + "id": "node/2103216538" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2126361721", + "addr:city": "Paris", + "addr:street": "Rue du Faubourg Montmartre", + "amenity": "restaurant", + "cuisine": "pizza", + "name": "Matteo", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3432485, + 48.8729655 + ] + }, + "id": "node/2126361721" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2164023035", + "amenity": "restaurant", + "cuisine": "asian", + "name": "Le Bonheur", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3435127, + 48.8730359 + ] + }, + "id": "node/2164023035" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2446448987", + "amenity": "restaurant", + "check_date": "2023-01-26", + "cuisine": "korean", + "name": "Sobane", + "opening_hours": "Mo-Sa 12:00-14:30, Mo-Su 19:00-22:30", + "smoking": "outside" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3448931, + 48.8788007 + ] + }, + "id": "node/2446448987" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2471187815", + "amenity": "restaurant", + "cuisine": "turkish", + "name": "Grill Istanbul", + "source": "survey 2013", + "survey:date": "2018-05-05" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3442886, + 48.8767035 + ] + }, + "id": "node/2471187815" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2485354450", + "amenity": "restaurant", + "name": "Jeanne-Aimée" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3384716, + 48.8762503 + ] + }, + "id": "node/2485354450" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2500457603", + "amenity": "restaurant", + "check_date:opening_hours": "2022-09-14", + "name": "Au Taquet", + "opening_hours:signed": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3459265, + 48.8758373 + ] + }, + "id": "node/2500457603" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2514493568", + "amenity": "restaurant", + "cuisine": "asian", + "name": "Traiteur Délice Cadet", + "phone": "+33148784192", + "source": "survey", + "survey:date": "2018-05-05" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3443424, + 48.8763927 + ] + }, + "id": "node/2514493568" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2680421252", + "addr:city": "Paris", + "addr:housenumber": "46", + "addr:postcode": "75009", + "addr:street": "Rue Richer", + "air_conditioning": "yes", + "amenity": "restaurant", + "cuisine": "pizza;pasta", + "delivery": "yes", + "description": "Situé à Paris 9, à quelques mètres des Grands Boulevards, PAPI est un restaurant italien spécialisé dans la conception de pizzas au levain. Le restaurant PAPI (Pasta Pizza) vous propose des pizzas à base de levain, pâtes fraîches artisanales et aussi, de", + "diet:vegetarian": "yes", + "email": "bonjour@papirestaurant.fr", + "name": "Papi Restaurant", + "opening_hours": "Mo-Tu,Sa 12:00-14:00,18:30-21:30", + "phone": "+33 1 71 27 77 65", + "reservation": "yes", + "takeaway": "yes", + "website": "https://www.papirestaurant.fr/", + "wheelchair": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3436798, + 48.8741108 + ] + }, + "id": "node/2680421252" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2692650140", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "17", + "contact:postcode": "75009", + "contact:street": "Rue de Trévise", + "cuisine": "ukrainian;russian", + "diet:vegetarian": "yes", + "name": "Kalinka", + "wheelchair": "limited" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3451402, + 48.8737471 + ] + }, + "id": "node/2692650140" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2692650157", + "amenity": "restaurant", + "cuisine": "regional", + "name": "Kolfe Jean", + "opening_hours": "Mo-Fr 12:00-15:00,19:00-22:00", + "phone": "+33 1 47 70 68 76", + "smoking": "outside" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3449978, + 48.8731623 + ] + }, + "id": "node/2692650157" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2692650176", + "amenity": "restaurant", + "check_date": "2023-07-05", + "cuisine": "pizza", + "delivery": "no", + "diet:vegetarian": "yes", + "name": "Le Bookie", + "outdoor_seating": "yes", + "payment:mastercard": "yes", + "payment:visa": "yes", + "reservation": "yes", + "takeaway": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3450913, + 48.8735309 + ] + }, + "id": "node/2692650176" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2799009841", + "addr:housenumber": "46", + "addr:street": "Rue du Faubourg Montmartre", + "amenity": "restaurant", + "cuisine": "chinese", + "name": "Les Pâtes Vivantes", + "name:zh": "活着的面条", + "opening_hours": "Mo-Fr 12:00-15:00; Sa-Su 12:00-15:30; Mo-Su 19:00-23:00", + "phone": "+33 145 231 021", + "source": "survey 2013", + "website": "https://www.lespatesvivantes.net/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3415841, + 48.8747214 + ] + }, + "id": "node/2799009841" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2799010416", + "amenity": "restaurant", + "cuisine": "asian", + "source": "survey 2013" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.341641, + 48.874684 + ] + }, + "id": "node/2799010416" + }, + { + "type": "Feature", + "properties": { + "@id": "node/2961160024", + "addr:city": "Paris", + "addr:housenumber": "24", + "addr:postcode": "75009", + "addr:street": "Rue du Faubourg Montmartre", + "amenity": "restaurant", + "capacity": "8", + "cuisine": "pizza;new_york_pizza", + "facebook": "https://facebook.com/nickspizza", + "name": "Nick's Pizza", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3430215, + 48.8734806 + ] + }, + "id": "node/2961160024" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3183025906", + "amenity": "restaurant", + "cuisine": "chinese;huoguo", + "name": "Les Trois Royaumes", + "name:zh": "諸葛烤魚", + "source": "cadastre-dgi-fr source : Direction Générale des Impôts - Cadastre. Mise à jour : 2011" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3439946, + 48.8741151 + ] + }, + "id": "node/3183025906" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3415271563", + "amenity": "restaurant", + "cuisine": "Chinese;Shanxi", + "name": "Restaurant Do Eat", + "opening_hours": "Tu-We 12:00-14:30,19:00-22:30", + "phone": "+33 9 52 67 26 57", + "takeaway": "yes", + "website": "https://www.doeatparis.fr" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3419096, + 48.8764622 + ] + }, + "id": "node/3415271563" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3415271564", + "addr:city": "Paris", + "addr:housenumber": "28", + "addr:postcode": "75009", + "addr:street": "Rue Lamartine", + "amenity": "restaurant", + "cuisine": "korean", + "name": "Gin Go Gae", + "name:fr": "Gin Go Gae" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3419539, + 48.8766024 + ] + }, + "id": "node/3415271564" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3438053658", + "amenity": "restaurant", + "cuisine": "japanese", + "name": "Fukushiyama", + "opening_hours": "Mo-Fr 12:00-15:00,18:30-23:00; Su 18:30-23:00", + "operator": "Shao Jean-Marc", + "phone": "+33 1 47 70 45 67", + "smoking": "outside", + "source": "survey:2015-03-28", + "takeaway": "yes", + "toilets:wheelchair": "no", + "website": "http://www.fukushiyama-75.fr/", + "wheelchair": "limited", + "wheelchair:description": "Petite marche à l'entrée, porte battante difficile à ouvrir. Toilettes en bas d'un escalier très étroit et raide." + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3445995, + 48.8759307 + ] + }, + "id": "node/3438053658" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3438081754", + "amenity": "restaurant", + "name": "La Gargamelle", + "outdoor_seating": "yes", + "phone": "+33 1 48 78 33 90" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3400064, + 48.8749633 + ] + }, + "id": "node/3438081754" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3535132540", + "amenity": "restaurant", + "brewery": "Estrella;Voll Damm", + "cuisine": "spanish", + "name": "Le Petit Barcelone" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3449321, + 48.873689 + ] + }, + "id": "node/3535132540" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3564245493", + "amenity": "restaurant", + "name": "Les Fils à Maman", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3437357, + 48.8735029 + ] + }, + "id": "node/3564245493" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3564245494", + "amenity": "restaurant", + "cuisine": "italian", + "description": "Caffé della Pizza Ristorante", + "name": "Caffé della Pizza", + "note:fr": "Caffé della Pizza Ristorante" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3433422, + 48.8733106 + ] + }, + "id": "node/3564245494" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3564245498", + "addr:housenumber": "7", + "addr:postcode": "75009", + "addr:street": "Rue Geoffroy Marie", + "amenity": "restaurant", + "cuisine": "italian", + "name": "Vale & Ale", + "opening_hours": "Mo-Su 12:00-14:00, 19:30-22:00", + "phone": "+33153346287", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3436472, + 48.8734527 + ] + }, + "id": "node/3564245498" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3588432323", + "amenity": "restaurant", + "cuisine": "chinese", + "name": "La Cuisine Chinoise", + "phone": "+33147702227", + "source": "survey 2015" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3444855, + 48.8751609 + ] + }, + "id": "node/3588432323" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3655998026", + "amenity": "restaurant", + "name": "Le Beaucé" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3435445, + 48.8739692 + ] + }, + "id": "node/3655998026" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3668191709", + "amenity": "restaurant", + "name": "Le Bistrot à deux Têtes", + "phone": "+33 1 48 78 35 58" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3403004, + 48.8756987 + ] + }, + "id": "node/3668191709" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3696663374", + "amenity": "restaurant", + "name": "Etoile du Liban", + "takeaway": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3384166, + 48.8776751 + ] + }, + "id": "node/3696663374" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3702655129", + "access:covid19": "no", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "25", + "contact:postcode": "75009", + "contact:street": "Rue Taitbout", + "cuisine": "indian", + "delivery:covid19": "yes", + "diet:vegan": "yes", + "diet:vegetarian": "yes", + "name": "New Balal" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.33519, + 48.8732905 + ] + }, + "id": "node/3702655129" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3811446658", + "amenity": "restaurant", + "cuisine": "french", + "name": "À Côté" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3350103, + 48.8734176 + ] + }, + "id": "node/3811446658" + }, + { + "type": "Feature", + "properties": { + "@id": "node/3837126776", + "addr:postcode": "75009", + "amenity": "restaurant", + "name": "Arlette", + "opening_hours": "Tu-Fr 12:00-14:30,19:00-22:00; Sa 12:00-14:30,19:00-22:30", + "operator": "Daskin Hakan", + "phone": "+33 9 80 59 21 67", + "smoking": "outside", + "website": "https://www.facebook.com/La-Pom-dAmour-746638862110531" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3409528, + 48.8766269 + ] + }, + "id": "node/3837126776" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4034254547", + "addr:housenumber": "70", + "addr:postcode": "75009", + "addr:street": "Rue La Fayette", + "amenity": "restaurant", + "happy_hours": "Mo-Su 16:00-22:00", + "name": "Le Régent", + "opening_hours": "Mo-Su 12:00-02:00", + "phone": "+33 1 48 78 14 69" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3441556, + 48.8758287 + ] + }, + "id": "node/4034254547" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4044332041", + "amenity": "restaurant", + "check_date": "2023-08-06", + "contact:city": "Paris", + "contact:housenumber": "21", + "contact:phone": "+33 9 83 87 95 95", + "contact:postcode": "75009", + "contact:street": "Rue Le Peletier", + "contact:website": "https://www.monph7.com/", + "diet:vegetarian": "only", + "gluten_free": "yes", + "name": "PH7", + "organic": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.338495, + 48.8729555 + ] + }, + "id": "node/4044332041" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4044482741", + "amenity": "restaurant", + "indoor_seating": "yes", + "name": "Xia ri leng yin", + "outdoor_seating": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3360738, + 48.8742719 + ] + }, + "id": "node/4044482741" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4125470311", + "amenity": "restaurant", + "contact:phone": "+33 9 50 77 53 35", + "cuisine": "chinese", + "name": "Aigle d'Orient", + "website": "https://www.aigledorient.com" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3444061, + 48.8769794 + ] + }, + "id": "node/4125470311" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4125470312", + "amenity": "restaurant", + "contact:phone": "+33 1 48 78 09 95", + "cuisine": "japanese", + "name": "Yoki", + "opening_hours": "18:00-23:00, Mo-Sa 10:30-14:30", + "website": "https://yokisushi.com" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3446736, + 48.877117 + ] + }, + "id": "node/4125470312" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4125480963", + "amenity": "restaurant", + "cuisine": "lebanese", + "name": "Al Bayader", + "phone": "+33 1 48 78 40 44" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3445038, + 48.8772828 + ] + }, + "id": "node/4125480963" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4143268694", + "addr:city": "Paris", + "addr:postcode": "75009", + "addr:street": "Rue Saint-Lazare", + "amenity": "restaurant", + "cuisine": "asian", + "name": "Moy Goi Cuon Bar", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3361925, + 48.8768325 + ] + }, + "id": "node/4143268694" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4143268696", + "addr:city": "Paris", + "addr:housenumber": "32", + "addr:postcode": "75009", + "addr:street": "Rue Saint-Lazare", + "amenity": "restaurant", + "name": "Barth", + "takeaway": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3362388, + 48.8768334 + ] + }, + "id": "node/4143268696" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4197369297", + "amenity": "restaurant", + "cuisine": "regional", + "name": "Plein Ouest" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3443814, + 48.8743663 + ] + }, + "id": "node/4197369297" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4207592630", + "addr:city": "Paris", + "addr:housenumber": "11", + "addr:postcode": "75009", + "addr:street": "Rue de Montyon", + "amenity": "restaurant", + "cuisine": "japanese", + "description": "Restaurant japonais gastronomique", + "name": "Sumibi Kaz", + "opening_hours": "Tu-Sa 12:00-14:30, 19:00-23:00", + "phone": "+33 1 45 80 26 98", + "website": "https://www.sumibi-kaz.com" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3441219, + 48.8730386 + ] + }, + "id": "node/4207592630" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4207596442", + "amenity": "restaurant", + "cuisine": "japanese", + "name": "Matsusaka" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3438082, + 48.8731589 + ] + }, + "id": "node/4207596442" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4278261392", + "addr:housenumber": "8", + "addr:postcode": "75009", + "addr:street": "Rue Geoffroy Marie", + "amenity": "restaurant", + "cuisine": "mexican", + "name": "Zicatela" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3440632, + 48.8734583 + ] + }, + "id": "node/4278261392" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4278626563", + "amenity": "restaurant", + "diet:kosher": "only", + "name": "Chez David", + "phone": "+33 1 40 22 61 05", + "smoking": "outside" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3442623, + 48.8730373 + ] + }, + "id": "node/4278626563" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4278626564", + "addr:city": "Paris", + "addr:housenumber": "11", + "addr:postcode": "75009", + "addr:street": "Rue de Montyon", + "amenity": "restaurant", + "cuisine": "thai", + "name": "Makham Thaï Paris", + "phone": "+33 1 47 70 40 95" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3439214, + 48.8730409 + ] + }, + "id": "node/4278626564" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4312773166", + "addr:city": "Paris", + "addr:housenumber": "47", + "addr:postcode": "75009", + "addr:street": "Rue Saint-Georges", + "amenity": "restaurant", + "cuisine": "turkish", + "name": "Sizin", + "takeaway": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3372674, + 48.8778223 + ] + }, + "id": "node/4312773166" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4360249193", + "addr:housenumber": "21", + "addr:postcode": "75009", + "addr:street": "Rue du Faubourg Montmartre", + "amenity": "restaurant", + "check_date": "2023-08-06", + "contact:facebook": "mamieburgerparis", + "contact:instagram": "mamierestaurants", + "contact:twitter": "mamieburger", + "cuisine": "burger", + "name": "Mamie", + "name:fr": "Mamia Burger", + "opening_hours": "Mo-Su 08:00-15:00, 18:00-02:00", + "phone": "+33 9 81 45 72 92", + "website": "https://www.mamie-restaurants.com/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3429045, + 48.8730594 + ] + }, + "id": "node/4360249193" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4366842297", + "addr:housenumber": "3", + "addr:street": "Rue Milton", + "amenity": "restaurant", + "cuisine": "brazilian", + "name": "Gabriela", + "name:fr": "Gabriela", + "phone": "+33 1 42 80 28 14", + "website": "http://www.gabriela.fr/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3404516, + 48.8770327 + ] + }, + "id": "node/4366842297" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4389641402", + "addr:city": "Paris", + "addr:housenumber": "18", + "addr:postcode": "75009", + "addr:street": "Rue Saint-Lazare", + "amenity": "restaurant", + "capacity": "20", + "cuisine": "french", + "description": "Restaurant bar à vin", + "drink": "wine", + "level": "1", + "name": "Le Bouclier de Bacchus", + "payment:credit_cards": "yes", + "smoking": "outside", + "website": "http://www.bouclierdebacchus.com/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3377035, + 48.8768607 + ] + }, + "id": "node/4389641402" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4412152990", + "amenity": "restaurant", + "changing_table": "no", + "cuisine": "japanese;noodle", + "level": "0", + "name": "Abri Soba", + "name:fr": "Abri Soba", + "outdoor_seating": "no", + "reservation": "no", + "smoking": "no", + "toilets": "yes", + "toilets:access": "customers", + "toilets:disposal": "flush", + "toilets:position": "seated", + "toilets:wheelchair": "no", + "wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3444884, + 48.8750272 + ] + }, + "id": "node/4412152990" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4475551548", + "addr:housenumber": "4", + "addr:street": "Rue Choron", + "amenity": "restaurant", + "name": "Choron", + "opening_hours:covid19": "off" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.342466, + 48.8773651 + ] + }, + "id": "node/4475551548" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4548570150", + "addr:housenumber": "24", + "addr:postcode": "75009", + "addr:street": "Rue Richer", + "amenity": "restaurant", + "cuisine": "indonesian", + "name": "Makan Makan", + "opening_hours": "Mo-Fr 12:00-14:30, 18:30-22:00; Sa 12:00-15:00", + "phone": "+33 6 27 06 97 01", + "source": "survey" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3458145, + 48.8741173 + ] + }, + "id": "node/4548570150" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4580960891", + "addr:housenumber": "60", + "addr:postcode": "75009", + "addr:street": "Rue La Fayette", + "amenity": "restaurant", + "cuisine": "french", + "name": "Les Diamantaires", + "phone": "+33147707814" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3425926, + 48.8754075 + ] + }, + "id": "node/4580960891" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4678487389", + "amenity": "restaurant", + "cuisine": "vietnamese", + "name": "Mô Ri" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3404799, + 48.8755603 + ] + }, + "id": "node/4678487389" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4680160490", + "addr:city": "Paris 9eme Arrondissement", + "addr:housenumber": "17", + "addr:postcode": "75009", + "addr:street": "Rue de Maubeuge", + "amenity": "restaurant", + "contact:facebook": "https://www.facebook.com/CoinOpTable", + "contact:twitter": "https://twitter.com/COINOPTABLE", + "cuisine": "burger;coffee_shop;french", + "diet:vegetarian": "yes", + "leisure": "amusement_arcade", + "name": "Coin-Op Table", + "opening_hours": "We-Sa 12:00-23:00, Tu 12:00-15:00, Su 12:00-17:00", + "phone": "+33140358537", + "website": "http://coin-op-table.com/", + "wheelchair": "limited" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3434569, + 48.8777223 + ] + }, + "id": "node/4680160490" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4753837525", + "amenity": "restaurant", + "cuisine": "italian", + "name": "Moriarty", + "name:fr": "Moriarty" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3421848, + 48.8766059 + ] + }, + "id": "node/4753837525" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4758384028", + "amenity": "restaurant", + "level": "0", + "name": "Fuxia", + "name:fr": "Fuxia" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3395866, + 48.8784544 + ] + }, + "id": "node/4758384028" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4790815426", + "addr:city": "Paris", + "addr:housenumber": "47", + "addr:postcode": "75009", + "addr:street": "Rue Richer", + "amenity": "restaurant", + "level": "0", + "name": "Bien Élevé", + "name:fr": "Bien Élevé", + "opening_hours": "Mo-Su 12:00-22:00", + "outdoor_seating": "yes", + "phone": "+33 1 45 81 44 35", + "website": "http://www.bieneleve.fr" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3432187, + 48.8739833 + ] + }, + "id": "node/4790815426" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4855673114", + "amenity": "restaurant", + "cuisine": "italian", + "name": "Chez Vincent" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3375197, + 48.8778834 + ] + }, + "id": "node/4855673114" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4855673116", + "amenity": "restaurant", + "cuisine": "french", + "name": "Le Bon Georges" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3372106, + 48.8776422 + ] + }, + "id": "node/4855673116" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4855693222", + "amenity": "restaurant", + "cuisine": "japanese", + "name": "MiZushi" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.337289, + 48.8770812 + ] + }, + "id": "node/4855693222" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4859813179", + "addr:city": "Paris", + "addr:housenumber": "24", + "addr:postcode": "75009", + "addr:street": "Rue Saint-Lazare", + "amenity": "restaurant", + "capacity": "10", + "cuisine": "japanese", + "disused:amenity": "fast_food", + "name": "Gyoza Bar", + "takeaway": "yes", + "takeaway:lunchbox": "unknown" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3369509, + 48.8767825 + ] + }, + "id": "node/4859813179" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4864147785", + "amenity": "restaurant", + "name": "Le Dream Café" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3394129, + 48.8770625 + ] + }, + "id": "node/4864147785" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4908987597", + "addr:city": "Paris", + "addr:housenumber": "7", + "addr:postcode": "75009", + "addr:street": "Rue Cadet", + "amenity": "restaurant", + "check_date": "2023-08-06", + "cuisine": "pizza", + "name": "Le Papacionu", + "opening_hours": "Mo-Sa 12:00-14:30,19:00-23:30", + "outdoor_seating": "yes", + "payment:credit_cards": "yes", + "payment:mastercard": "yes", + "payment:visa": "yes", + "smoking": "outside", + "survey:date": "2018-05-05", + "takeaway": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3427513, + 48.8747685 + ] + }, + "id": "node/4908987597" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4935628429", + "amenity": "restaurant", + "name": "La Condesa" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3427482, + 48.8785156 + ] + }, + "id": "node/4935628429" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4935628430", + "addr:housenumber": "8", + "addr:street": "Rue Hippolyte Lebas", + "amenity": "restaurant", + "description": "Neo-bistrot", + "name": "Caillebotte", + "opening_hours": "Mo-Fr 12:30-14:30,19:30-22:30", + "phone": "+33153208870" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.34083, + 48.8771 + ] + }, + "id": "node/4935628430" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4966817508", + "amenity": "restaurant", + "name": "libshop" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.340406, + 48.8750922 + ] + }, + "id": "node/4966817508" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4966817509", + "amenity": "restaurant", + "contact:mobile": "+33 6 52 22 44 64", + "cuisine": "regional", + "name": "Le P'tit Piano", + "opening_hours": "Mo 07:00-16:00, Tu-Fr 07:00-24:00, Sa-Su 10:00-24:00", + "website": "https://leptitpianobar.fr" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3436884, + 48.8756444 + ] + }, + "id": "node/4966817509" + }, + { + "type": "Feature", + "properties": { + "@id": "node/4973211216", + "addr:city": "Paris", + "addr:postcode": "75009", + "addr:street": "Rue de Châteaudun", + "amenity": "restaurant", + "cuisine": "greek", + "diet:vegan": "yes", + "diet:vegetarian": "yes", + "indoor_seating": "yes", + "name": "Gallika", + "opening_hours": "Mo-Fr 11:30-14:30", + "outdoor_seating": "yes", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.336314, + 48.8760016 + ] + }, + "id": "node/4973211216" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5029994921", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "23", + "contact:phone": "+33 1 55 07 86 52", + "contact:postcode": "75009", + "contact:street": "Rue de la Victoire", + "name": "Louis" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3387445, + 48.8750532 + ] + }, + "id": "node/5029994921" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5132372221", + "addr:housenumber": "31 bis", + "addr:postcode": "75009", + "addr:street": "Rue du Faubourg Montmartre", + "amenity": "restaurant", + "check_date": "2023-08-06", + "cuisine": "argentinian", + "email": "hola@locolerestaurant.com", + "name": "Loco", + "phone": "+33 970986841", + "website": "http://www.locolerestaurant.com" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3424946, + 48.8737307 + ] + }, + "id": "node/5132372221" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5186169364", + "amenity": "restaurant", + "cuisine": "pizza", + "name": "Arcimboldo", + "opening_hours": "Tu-Fr 12:00-14:30, Tu-Sa 19:00-22:00", + "phone": "+33 1 48 78 35 54", + "website": "https://arcimboldopizza.fr" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3434194, + 48.8753151 + ] + }, + "id": "node/5186169364" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5196307522", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "11", + "contact:phone": "+33 1 48 24 84 40", + "contact:postcode": "75009", + "contact:street": "Rue Cadet", + "contact:website": "http://royalcadet.fr", + "cuisine": "french", + "name": "Le Royal Cadet", + "name:en": "Brasserie Cadet", + "survey:date": "2018-05-05" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3431504, + 48.8752635 + ] + }, + "id": "node/5196307522" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5209260098", + "addr:postcode": "75009", + "amenity": "restaurant", + "check_date": "2023-08-06", + "cuisine": "japanese", + "name": "Neko Ramen", + "opening_hours": "Mo-Su 11:30-23:00", + "website": "https://www.nekoramen.fr/", + "wheelchair": "limited" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3423234, + 48.8731313 + ] + }, + "id": "node/5209260098" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5302471392", + "addr:city": "Paris", + "addr:housenumber": "15", + "addr:postcode": "75009", + "addr:street": "Rue Notre-Dame-de-Lorette", + "amenity": "restaurant", + "cuisine": "asian", + "name": "Mian", + "website": "https://mianfan.fr" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3380986, + 48.8776793 + ] + }, + "id": "node/5302471392" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5417941785", + "addr:city": "Paris", + "addr:housenumber": "8", + "addr:postcode": "75009", + "addr:street": "Rue de Châteaudun", + "amenity": "restaurant", + "cuisine": "world;healthy", + "email": "contact@nousrestaurant.fr", + "name": "Nous Châteaudun", + "opening_hours": "Mo-Fr 12:00-14:30,19:00-22:30; Sa,Su 12:00-15:00,19:00-22:30", + "smoking": "no", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes", + "website": "http://www.nousrestaurant.fr" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3414237, + 48.8759011 + ] + }, + "id": "node/5417941785" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5667593336", + "amenity": "restaurant", + "cuisine": "lebanese", + "name": "Sannine", + "outdoor_seating": "yes", + "source": "survey 2018" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3426956, + 48.8738696 + ] + }, + "id": "node/5667593336" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5667593337", + "amenity": "restaurant", + "name": "Chez Léon", + "source": "survey 2018" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3428335, + 48.8737555 + ] + }, + "id": "node/5667593337" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5689953454", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "27", + "contact:phone": "+33 9 70 38 61 00", + "contact:postcode": "75009", + "contact:street": "Rue Richer", + "contact:website": "https://www.lessardignac.fr/", + "cuisine": "regional;wine", + "diet:vegetarian": "yes", + "name": "Les Sardignac", + "opening_hours": "Mo-Fr 12:00-15:00,18:00-02:00; Sa 18:00-02:00; Su off", + "outdoor_seating": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3449879, + 48.8739562 + ] + }, + "id": "node/5689953454" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5774910156", + "amenity": "restaurant", + "brand": "Pizza Hut", + "brand:wikidata": "Q191615", + "cuisine": "pizza", + "drive_through": "no", + "name": "Pizza Hut", + "opening_hours": "Mo-Fr 11:30-14:30,18:00-23:00; Sa,Su,PH 11:30-23:00", + "operator": "Pizza Hut" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3444669, + 48.8771365 + ] + }, + "id": "node/5774910156" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5877960608", + "addr:city": "Paris", + "addr:housenumber": "7", + "addr:postcode": "75009", + "addr:street": "Rue Bourdaloue", + "amenity": "restaurant", + "capacity": "20", + "cuisine": "latin_american", + "name": "Isana", + "smoking": "no", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3385935, + 48.8765862 + ] + }, + "id": "node/5877960608" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5913758023", + "amenity": "restaurant", + "contact:email": "nathan@lacaleducotentin.fr", + "contact:phone": "+33 6 23917982", + "contact:website": "https://www.lacaleducotentin.fr/", + "cuisine": "oyster", + "name": "La Cale du Cotentin" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3401432, + 48.8771027 + ] + }, + "id": "node/5913758023" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5916181911", + "addr:city": "Paris", + "addr:housenumber": "29", + "addr:postcode": "75009", + "addr:street": "Rue Saint-Georges", + "amenity": "restaurant", + "capacity": "10", + "name": "Pick and Co", + "smoking": "no", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3370123, + 48.8764072 + ] + }, + "id": "node/5916181911" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5980951946", + "addr:city": "Paris 9eme Arrondissement", + "addr:housenumber": "18", + "addr:postcode": "75009", + "addr:street": "Rue Rodier", + "amenity": "restaurant", + "cuisine": "japanese", + "dog": "no", + "name": "Hotaru", + "opening_hours": "Tu-Sa 12:30-14:30,19:00-22:00", + "payment:mastercard": "yes", + "payment:visa": "yes", + "phone": "+33 1 48 78 33 74", + "smoking": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3427222, + 48.8782074 + ] + }, + "id": "node/5980951946" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5981803288", + "amenity": "restaurant", + "cuisine": "french", + "name": "Kozo" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3373363, + 48.8772425 + ] + }, + "id": "node/5981803288" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5981805085", + "amenity": "restaurant", + "contact:city": "Paris 9eme Arrondissement", + "contact:housenumber": "48", + "contact:phone": "+33 1 42 81 35 94", + "contact:postcode": "75009", + "contact:street": "Rue Saint-Georges", + "name": "Chez Delphine", + "opening_hours": "We-Fr 12:00-14:30, 19:00-22:30; Sa 19:00-22:30", + "ref:FR:SIRET": "85007565600015", + "website": "https://www.restaurantchezdelphine.fr/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3373158, + 48.8771873 + ] + }, + "id": "node/5981805085" + }, + { + "type": "Feature", + "properties": { + "@id": "node/5981805185", + "amenity": "restaurant", + "cuisine": "tibetan", + "diet:vegetarian": "yes", + "name": "Nirvana Dream" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3372412, + 48.8769371 + ] + }, + "id": "node/5981805185" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6003131457", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "2", + "contact:postcode": "75009", + "contact:street": "Rue de Provence", + "cuisine": "italian;pizza", + "name": "Il Piccolo Drouot", + "takeaway": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3417958, + 48.8741188 + ] + }, + "id": "node/6003131457" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6007703105", + "addr:city": "Paris", + "addr:housenumber": "31", + "addr:postcode": "75009", + "addr:street": "Rue Saint-Lazare", + "amenity": "restaurant", + "cuisine": "asian", + "name": "Mizupoke", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3361794, + 48.8766901 + ] + }, + "id": "node/6007703105" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6044004255", + "addr:city": "Paris", + "addr:housenumber": "37", + "addr:postcode": "75009", + "addr:street": "Rue du Faubourg Montmartre", + "amenity": "restaurant", + "email": "hello@beauetfort.com", + "level": "0", + "name": "Beau & Fort", + "opening_hours": "Mo-Sa 10:00-23:00,Su 10:30-16:00", + "outdoor_seating": "yes", + "phone": "+33 1 45 89 23 96", + "smoking": "outside", + "website": "https://www.beauetfort.com/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3420981, + 48.8741043 + ] + }, + "id": "node/6044004255" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6044106913", + "amenity": "restaurant", + "level": "0", + "name": "La poketerie" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3417873, + 48.8739937 + ] + }, + "id": "node/6044106913" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6044132983", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:email": "contact@thams.fr", + "contact:housenumber": "11", + "contact:phone": "+33 9 84 55 67 40", + "contact:postcode": "75009", + "contact:street": "Rue de Provence", + "contact:website": "http://www.thams.fr/", + "cuisine": "vietnamese;lao;taiwanese", + "level": "0", + "name": "Tham's", + "outdoor_seating": "no", + "smoking": "no", + "takeaway": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3410975, + 48.874012 + ] + }, + "id": "node/6044132983" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6044324520", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "54", + "contact:phone": "+33 1 45 23 18 29", + "contact:postcode": "75009", + "contact:street": "Rue Richer", + "cuisine": "sushi;japanese", + "level": "0", + "name": "Line Sushi", + "outdoor_seating": "no", + "smoking": "no", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3431805, + 48.8740953 + ] + }, + "id": "node/6044324520" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6068528486", + "amenity": "restaurant", + "cuisine": "chinese", + "name": "Au Bonheur", + "old_name": "Orient Express" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3430313, + 48.875112 + ] + }, + "id": "node/6068528486" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6112669236", + "addr:city": "Paris", + "addr:housenumber": "57", + "addr:postcode": "75009", + "addr:street": "Rue du Faubourg Montmartre", + "amenity": "restaurant", + "diet:vegetarian": "yes", + "name": "Sainbol", + "phone": "+33 9 52 59 37 20", + "smoking": "no", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes", + "website": "http://sainbol.com/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3405363, + 48.8754953 + ] + }, + "id": "node/6112669236" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6140347085", + "amenity": "restaurant", + "name": "Saveur du Si Chuan", + "opening_hours": "Tu-Su 12:00-15:00,18:30-22:30" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3409007, + 48.8753895 + ] + }, + "id": "node/6140347085" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6251324473", + "amenity": "restaurant", + "cuisine": "asian;vietnamese", + "name": "Zen Bo Bun", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3362409, + 48.8766857 + ] + }, + "id": "node/6251324473" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6288844557", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "12", + "contact:postcode": "75009", + "contact:street": "Rue Marguerite de Rochechouart", + "cuisine": "french", + "name": "Les Anges Gourmands", + "phone": "+33 1 48 78 28 83", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3446931, + 48.8771701 + ] + }, + "id": "node/6288844557" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6325739385", + "addr:housenumber": "7", + "addr:postcode": "75009", + "addr:street": "Rue de Châteaudun", + "amenity": "restaurant", + "name": "Little Baobai" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3414884, + 48.8756793 + ] + }, + "id": "node/6325739385" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6388821014", + "amenity": "restaurant", + "name": "Simone Lemon", + "opening_hours": "Mo-Fr 11:45-14:45, 09:00-18:00; Sa, Su 11:30-16:00", + "website": "https://www.simonelemon.com/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3393076, + 48.8742544 + ] + }, + "id": "node/6388821014" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6420966885", + "addr:city": "Paris", + "addr:housenumber": "19", + "addr:postcode": "75009", + "addr:street": "Rue Notre-Dame-de-Lorette", + "amenity": "restaurant", + "cuisine": "venezuelan", + "email": "contact@ajidulce.fr", + "internet_access": "wlan", + "name": "aji dulce", + "name:en": "aji dulce - the taste of Venezuela", + "name:fr": "aji dulce - le goût du Venezuela", + "name:nl": "aji dulce - de smaak van Venezuela", + "phone": "+33 1 83 87 15 56", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes", + "website": "http://www.ajidulce.fr" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3379234, + 48.8778931 + ] + }, + "id": "node/6420966885" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6554151627", + "addr:housenumber": "18", + "addr:street": "Rue Chauchat", + "amenity": "restaurant", + "name": "Palinuro", + "opening_hours": "Mo-Sa 09:00-15:00, 18:00-23:00", + "phone": "+33 1 47 70 94 75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3397907, + 48.8738792 + ] + }, + "id": "node/6554151627" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6562769648", + "amenity": "restaurant", + "capacity": "8", + "contact:city": "Paris", + "contact:housenumber": "34", + "contact:phone": "+33 1 40 34 21 80", + "contact:postcode": "75009", + "contact:street": "Rue de la Victoire", + "contact:website": "https://www.uglywok.com/", + "cuisine": "thai", + "name": "The Ugly Wok", + "opening_hours": "Mo-Fr 12:00-15:00", + "smoking": "no", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3375723, + 48.8751921 + ] + }, + "id": "node/6562769648" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6602352485", + "addr:housenumber": "20", + "addr:street": "Rue des Martyrs", + "amenity": "restaurant", + "cuisine": "chinese", + "name": "Yoom Dim Sum" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3396821, + 48.877974 + ] + }, + "id": "node/6602352485" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6776083685", + "addr:housenumber": "8", + "addr:street": "Rue Cadet", + "amenity": "restaurant", + "name": "Mam Thai", + "opening_hours": "Mo-Fr 11:30-16:00, 18:00-22:30; Sa-Su 00:00-24:00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.342717, + 48.8744817 + ] + }, + "id": "node/6776083685" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6791192885", + "amenity": "restaurant", + "name": "Muqam" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3455731, + 48.8752333 + ] + }, + "id": "node/6791192885" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6802180924", + "addr:city": "Paris 9eme Arrondissement", + "addr:housenumber": "17", + "addr:postcode": "75009", + "addr:street": "Rue de Maubeuge", + "amenity": "restaurant", + "cuisine": "chinese;vietnamese", + "name": "Phô Neuf", + "opening_hours": "Tu-Su 12:00-14:30,18:30-22:30", + "phone": "+33148781114" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3435552, + 48.877757 + ] + }, + "id": "node/6802180924" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6905693428", + "addr:city": "Paris", + "addr:housenumber": "43", + "addr:postcode": "75009", + "addr:street": "Rue Laffitte", + "amenity": "restaurant", + "contact:instagram": "majouja.paris", + "cuisine": "arab", + "indoor_seating": "yes", + "name": "Les Piplettes", + "name:en": "Majouja", + "name:es": "Majouja", + "name:fr": "Majouja", + "opening_hours": "Tu-Th 12:00-15:00; Fr-Sa 12:00-15:00, 19:30-22:00", + "outdoor_seating": "yes", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes", + "website": "https://www.majoujaparis.fr/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3384016, + 48.8753137 + ] + }, + "id": "node/6905693428" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6909732685", + "amenity": "restaurant", + "name": "Paris Yum" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3456359, + 48.8758733 + ] + }, + "id": "node/6909732685" + }, + { + "type": "Feature", + "properties": { + "@id": "node/6987051316", + "amenity": "restaurant", + "name": "Café Drouot" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3406278, + 48.8729446 + ] + }, + "id": "node/6987051316" + }, + { + "type": "Feature", + "properties": { + "@id": "node/7004752103", + "amenity": "restaurant", + "changing_table": "no", + "check_date:opening_hours": "2023-04-29", + "cuisine": "ramen;noodle;japanese", + "delivery": "yes", + "internet_access": "wlan", + "internet_access:fee": "no", + "level": "0", + "name": "Naruto Ramen", + "opening_hours": "Su-Th 12:00-22:30, Fr-Sa 12:00-23:00", + "phone": "+33 1 71 60 45 82", + "smoking": "no", + "takeaway": "yes", + "toilets": "yes", + "toilets:access": "customers", + "toilets:wheelchair": "no", + "website": "https://naruto-ramen.fr/fr", + "wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.340611, + 48.8754081 + ] + }, + "id": "node/7004752103" + }, + { + "type": "Feature", + "properties": { + "@id": "node/7011199779", + "amenity": "restaurant", + "name": "Corner Haussmann" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3351626, + 48.8728637 + ] + }, + "id": "node/7011199779" + }, + { + "type": "Feature", + "properties": { + "@id": "node/7124999574", + "addr:housenumber": "15", + "addr:street": "Rue Lamartine", + "amenity": "restaurant", + "cuisine": "chinese", + "name": "Miss SUN", + "name:fr": "Miss SUN", + "name:zh": "遇见长安", + "opening_hours": "Mo-Th 18:30-23:00; Fr-Su 12:30-15:30,18:30-23:00", + "payment:cash": "yes", + "payment:credit_cards": "no", + "payment:debit_cards": "no", + "payment:mastercard": "yes", + "payment:visa": "yes", + "phone": "+33 6 18 85 91 87;+33 1 42 45 37 87", + "reservation": "yes", + "smoking": "no", + "toilets": "yes", + "toilets:wheelchair": "no", + "wheelchair": "limited" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3420687, + 48.8764516 + ] + }, + "id": "node/7124999574" + }, + { + "type": "Feature", + "properties": { + "@id": "node/7211522063", + "addr:city": "Paris", + "addr:housenumber": "37", + "addr:postcode": "75009", + "addr:street": "Rue La Fayette", + "amenity": "restaurant", + "contact:city": "Paris", + "contact:housenumber": "37", + "contact:postcode": "75009", + "contact:street": "Rue La Fayette", + "contact:website": "http://www.come-paris.fr", + "cuisine": "bowl", + "indoor_seating": "yes", + "name": "Côme - La Fayette", + "opening_hours": "Mo-Fr 10:00-15:15", + "outdoor_seating": "yes", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes", + "toilets": "yes", + "website": "http://www.come-paris.fr" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3386851, + 48.8746195 + ] + }, + "id": "node/7211522063" + }, + { + "type": "Feature", + "properties": { + "@id": "node/7227689604", + "addr:city": "Paris", + "addr:housenumber": "55", + "addr:postcode": "75009", + "addr:street": "Rue du Faubourg Montmartre", + "all_you_can_eat": "yes", + "amenity": "restaurant", + "cuisine": "japanese", + "name": "OKITO Le Peletier", + "opening_hours": "Mo-Sa 12:00-15:00,18:30-23:00; Su 18:30-23:00", + "phone": "+33140160923", + "website": "https://lepeletier.okito.fr" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3406528, + 48.8753639 + ] + }, + "id": "node/7227689604" + }, + { + "type": "Feature", + "properties": { + "@id": "node/7289213938", + "addr:city": "Paris", + "addr:housenumber": "18", + "addr:postcode": "75009", + "addr:street": "Rue Saint-Lazare", + "amenity": "restaurant", + "cuisine": "regional", + "name": "Chamaille", + "opening_hours": "Mo-Fr 12:00-14:00; Sa-Su 12:00-16:30", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes", + "website": "https://www.chamailleparis.fr/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.337638, + 48.8768523 + ] + }, + "id": "node/7289213938" + }, + { + "type": "Feature", + "properties": { + "@id": "node/7332920586", + "addr:housenumber": "4", + "addr:postcode": "75009", + "addr:street": "Rue Milton", + "amenity": "restaurant", + "name": "Le Soma (Milton)" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3407269, + 48.8769186 + ] + }, + "id": "node/7332920586" + }, + { + "type": "Feature", + "properties": { + "@id": "node/7926078392", + "addr:housenumber": "46", + "addr:street": "Rue Lamartine", + "amenity": "restaurant", + "cuisine": "african", + "name": "Les marmites de Fa", + "opening_hours": "Mo-Sa 11:00-15:00; Mo-We,Su 19:00-24:00; Th-Sa 19:00-02:00", + "phone": "+33 1 48 74 28 54", + "website": "http://www.lesmarmitesdefa.com" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3407289, + 48.8766369 + ] + }, + "id": "node/7926078392" + }, + { + "type": "Feature", + "properties": { + "@id": "node/7995599887", + "amenity": "restaurant", + "changing_table": "no", + "cuisine": "chinese;yunnan", + "delivery": "yes", + "level": "0", + "microbrewery": "no", + "name": "Carnet de route", + "opening_hours": "Th-Tu 12:00-15:00,18:30-22:30", + "phone": "+33 1 77 19 55 73", + "self_service": "no", + "smoking": "no", + "takeaway": "yes", + "toilets": "yes", + "toilets:access": "customers", + "toilets:disposal": "flush", + "toilets:position": "seated", + "toilets:wheelchair": "no", + "wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3403982, + 48.8756506 + ] + }, + "id": "node/7995599887" + }, + { + "type": "Feature", + "properties": { + "@id": "node/8005907666", + "addr:city": "Paris", + "addr:housenumber": "34", + "addr:postcode": "75009", + "addr:street": "Rue Notre-Dame-de-Lorette", + "amenity": "restaurant", + "contact:facebook": "https://www.facebook.com/thecurerestaurantparis/", + "contact:instagram": "https://www.instagram.com/thecurerestaurantparis/", + "cuisine": "bowl", + "diet:vegan": "yes", + "diet:vegetarian": "yes", + "email": "contact@thecure-restaurant.com", + "name": "The cure", + "opening_hours": "Mo-Fr 12:00-15:00", + "phone": "+33 9 82 53 53 73", + "takeaway": "yes", + "website": "https://thecure-restaurant.com/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3373252, + 48.878709 + ] + }, + "id": "node/8005907666" + }, + { + "type": "Feature", + "properties": { + "@id": "node/8714652617", + "addr:housenumber": "9", + "addr:postcode": "75009", + "addr:street": "Rue Rodier", + "amenity": "restaurant", + "contact:facebook": "troisfoisplusdepiment.fr", + "contact:instagram": "plus.de.piment", + "cuisine": "asian;chinese", + "email": "admin@3foisplusdepiment.com", + "name": "Trois Fois Plus de Piment", + "opening_hours": "Tu-Su 12:00-15:00, 18:30-22:30", + "phone": "+33 9 75 18 04 50", + "website": "https://troisfoisplusdepiment.fr/fr/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3423903, + 48.8778682 + ] + }, + "id": "node/8714652617" + }, + { + "type": "Feature", + "properties": { + "@id": "node/8853618432", + "amenity": "restaurant", + "cuisine": "uzbek", + "name": "Bukhara", + "phone": "+33 148241742", + "website": "http://ouzbek-resto.com" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3453171, + 48.8750543 + ] + }, + "id": "node/8853618432" + }, + { + "type": "Feature", + "properties": { + "@id": "node/8863039589", + "amenity": "restaurant", + "cuisine": "salad", + "indoor_seating": "yes", + "name": "Composé", + "outdoor_seating": "yes", + "takeaway": "yes", + "takeaway:customer_container": "yes", + "takeaway:lunchbox": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3366502, + 48.8759575 + ] + }, + "id": "node/8863039589" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9020241824", + "amenity": "restaurant", + "name": "Poké bar" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.345039, + 48.8781221 + ] + }, + "id": "node/9020241824" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9132781020", + "amenity": "restaurant", + "cuisine": "sushi", + "name": "Sushi Boubou" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3458015, + 48.8784761 + ] + }, + "id": "node/9132781020" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9161008838", + "amenity": "restaurant", + "name": "Kinn Khao" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3450698, + 48.8773404 + ] + }, + "id": "node/9161008838" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9455171462", + "amenity": "restaurant", + "cuisine": "asian", + "name": "Saveur Zen" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3408777, + 48.8759383 + ] + }, + "id": "node/9455171462" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9455171476", + "amenity": "restaurant", + "name": "Bercail" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3409963, + 48.8763092 + ] + }, + "id": "node/9455171476" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9455171483", + "amenity": "restaurant", + "cuisine": "indian", + "name": "Swagat" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3423886, + 48.8753512 + ] + }, + "id": "node/9455171483" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9455171486", + "amenity": "restaurant", + "name": "Nautilus" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3418263, + 48.8756523 + ] + }, + "id": "node/9455171486" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9528274317", + "addr:housenumber": "12", + "addr:postcode": "75009", + "addr:street": "Rue de Trévise", + "amenity": "restaurant", + "name": "La bouche bleue", + "website": "https://labouchebleue.com/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3452881, + 48.8733685 + ] + }, + "id": "node/9528274317" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9546668217", + "addr:housenumber": "8", + "addr:postcode": "75009", + "addr:street": "Rue Saint-Lazare", + "amenity": "restaurant", + "name": "Bleu Bao" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3382922, + 48.8769483 + ] + }, + "id": "node/9546668217" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9558699352", + "amenity": "restaurant", + "cuisine": "burger", + "name": "First LAP" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3404889, + 48.878225 + ] + }, + "id": "node/9558699352" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9558735500", + "amenity": "restaurant", + "butcher": "poultry", + "name": "Chez Plume", + "shop": "butcher" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3394079, + 48.8770283 + ] + }, + "id": "node/9558735500" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9558747717", + "amenity": "restaurant", + "cuisine": "asian", + "name": "Shizuka" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3400432, + 48.8783109 + ] + }, + "id": "node/9558747717" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9558784417", + "amenity": "restaurant", + "name": "Poppy" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3405847, + 48.8767842 + ] + }, + "id": "node/9558784417" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9558798517", + "amenity": "restaurant", + "name": "Creime" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3416793, + 48.876621 + ] + }, + "id": "node/9558798517" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9569512617", + "amenity": "restaurant", + "cuisine": "thai", + "name": "Prik Thaï" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3456865, + 48.8787423 + ] + }, + "id": "node/9569512617" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9609564349", + "amenity": "restaurant", + "name": "La démesure" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3415617, + 48.874518 + ] + }, + "id": "node/9609564349" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9610322971", + "amenity": "restaurant", + "cuisine": "asian" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3414998, + 48.8745113 + ] + }, + "id": "node/9610322971" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9610338336", + "amenity": "restaurant", + "indoor_seating": "yes", + "name": "Table neuf", + "opening_hours": "Mo-Sa 12:00-15:00,19:00-23:00", + "outdoor_seating": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3442234, + 48.8735546 + ] + }, + "id": "node/9610338336" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9610343518", + "addr:housenumber": "47", + "addr:postcode": "75009", + "addr:street": "Rue du Faubourg Montmartre", + "amenity": "restaurant", + "contact:facebook": "CaramelSarrasin", + "contact:instagram": "caramel_sarrasin", + "cuisine": "crepe", + "internet_access": "wlan", + "name": "Caramel Sarrasin", + "opening_hours": "Mo-We 10:30-14:00; Th-Fr 10:30-14:30, 19:00-22:00", + "phone": "09 51 25 80 04", + "website": "https://caramelsarrasin.com/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3413623, + 48.874617 + ] + }, + "id": "node/9610343518" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9629671717", + "addr:housenumber": "15", + "addr:postcode": "75009", + "addr:street": "Rue Hippolyte Lebas", + "amenity": "restaurant", + "name": "Pompette" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3400432, + 48.8771167 + ] + }, + "id": "node/9629671717" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9741286318", + "addr:housenumber": "56", + "addr:postcode": "75009", + "addr:street": "Rue Richer", + "amenity": "restaurant", + "cuisine": "thai", + "name": "Ma Cantine Thaï", + "opening_hours": "Mo-Fr 12:00-15:00", + "phone": "+33 144830230" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3428812, + 48.8741017 + ] + }, + "id": "node/9741286318" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9741488318", + "addr:housenumber": "56", + "addr:postcode": "75009", + "addr:street": "Rue Richer", + "amenity": "restaurant", + "cuisine": "thai", + "name": "Ma Cantine Thaï", + "opening_hours": "Mo-Fr 12:00-15:00", + "phone": "+33 144830230" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3428664, + 48.8740904 + ] + }, + "id": "node/9741488318" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9781734723", + "addr:city": "Paris", + "addr:country": "FR", + "addr:housenumber": "17", + "addr:postcode": "75009", + "addr:street": "Rue Bleue", + "amenity": "restaurant", + "cuisine": "armenian", + "name": "Cantine de la Maison de la Culture Arménienne de Paris", + "opening_hours": "Mo-Sa 12:00-15:00,19:00-23:00", + "operator": "Maison de la Culture Arménienne", + "phone": "+33 1 48 24 63 89", + "website": "https://www.facebook.com/Cantine-de-la-Maison-de-la-Culture-Armenienne-de-Paris-298995103835749" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3461182, + 48.8753538 + ] + }, + "id": "node/9781734723" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9801114117", + "amenity": "restaurant", + "name": "La Cave Drouot" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3406438, + 48.8727548 + ] + }, + "id": "node/9801114117" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9801172317", + "amenity": "restaurant", + "name": "Lupo caffè" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3425908, + 48.8730747 + ] + }, + "id": "node/9801172317" + }, + { + "type": "Feature", + "properties": { + "@id": "node/9864287812", + "amenity": "restaurant", + "cuisine": "thai", + "name": "Au Petit Te" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3380088, + 48.8778002 + ] + }, + "id": "node/9864287812" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10027954178", + "amenity": "restaurant", + "name": "Miznon" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3426102, + 48.872949 + ] + }, + "id": "node/10027954178" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10264645009", + "addr:housenumber": "17", + "addr:street": "Rue Bleue", + "amenity": "restaurant", + "name": "Restaurant Arménien - ambassade Arménie" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3461153, + 48.8757625 + ] + }, + "id": "node/10264645009" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10286310509", + "addr:housenumber": "17", + "addr:street": "Rue Le Peletier", + "amenity": "restaurant", + "check_date": "2023-08-06", + "cuisine": "chinese;fish", + "internet_access": "wlan", + "name": "Restaurant Sichuan", + "name:en": "Sichuan restaurant", + "name:zh": "川里川外", + "opening_hours": "Mo-Tu 12:00-14:30,18:30-22:30; Th-Su 12:00-14:30,18:30-22:30", + "payment:mastercard": "yes", + "payment:visa": "yes", + "phone": "+33 1 47 70 64 11" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3384403, + 48.872776 + ] + }, + "id": "node/10286310509" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10308274409", + "addr:street": "Rue Saint-Lazare", + "amenity": "restaurant", + "cuisine": "pizza", + "name": "Pépé Ronchon" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3360435, + 48.8767002 + ] + }, + "id": "node/10308274409" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10310449609", + "addr:housenumber": "31", + "addr:street": "Rue de Châteaudun", + "amenity": "restaurant", + "indoor_seating": "yes", + "name": "Pokawa", + "opening_hours": "Mo-Su 11:00-14:30, 18:30-22:30", + "outdoor_seating": "no", + "phone": "+33185736972", + "website": "https://restaurants.pokawa.com/poke-bowl-paris-chateaudun/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3369341, + 48.8759752 + ] + }, + "id": "node/10310449609" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10316518609", + "addr:housenumber": "20", + "addr:street": "Rue Milton", + "amenity": "restaurant", + "name": "Batoù" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.341216, + 48.8782605 + ] + }, + "id": "node/10316518609" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10542849003", + "addr:city": "Paris", + "addr:housenumber": "43", + "addr:postcode": "75009", + "addr:street": "Rue La Fayette", + "amenity": "restaurant", + "cuisine": "mediterranean", + "diet:non-vegetarian": "yes", + "diet:vegetarian": "yes", + "indoor_seating": "yes", + "name": "Med'eat", + "opening_hours": "Mo-Fr 11:45-14:15", + "outdoor_seating": "yes", + "phone": "+33 1 40 03 85 73", + "website": "https://www.medeat.fr/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3395568, + 48.8748583 + ] + }, + "id": "node/10542849003" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10571177714", + "amenity": "restaurant", + "name": "Alleudium" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3429008, + 48.8784676 + ] + }, + "id": "node/10571177714" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10604080605", + "addr:housenumber": "43", + "addr:street": "Rue Laffitte", + "amenity": "restaurant", + "name": "Fimmina Pizzeria", + "opening_hours": "Su-Fr 12:00-22:30" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3383617, + 48.8753047 + ] + }, + "id": "node/10604080605" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10613824711", + "amenity": "restaurant", + "cuisine": "french", + "name": "Berrie" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3355468, + 48.8766339 + ] + }, + "id": "node/10613824711" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10613829210", + "amenity": "restaurant", + "cuisine": "asian", + "diet:vegetarian": "no", + "name": "Bistro Dam Korean BBQ" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3364498, + 48.8766783 + ] + }, + "id": "node/10613829210" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10660907862", + "addr:housenumber": "17", + "addr:street": "Rue de Châteaudun", + "amenity": "restaurant", + "check_date": "2023-02-14", + "cuisine": "noodle;ramen;japanese", + "name": "Yatai Ramen", + "payment:contactless": "yes", + "payment:credit_cards": "yes" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3391516, + 48.8758331 + ] + }, + "id": "node/10660907862" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10682577392", + "amenity": "restaurant", + "name": "Mamou" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3354032, + 48.8738544 + ] + }, + "id": "node/10682577392" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10801608378", + "amenity": "restaurant", + "name": "Lorette" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3380722, + 48.8767795 + ] + }, + "id": "node/10801608378" + }, + { + "type": "Feature", + "properties": { + "@id": "node/10876238296", + "addr:city": "Paris", + "addr:housenumber": "23", + "addr:postcode": "75009", + "addr:street": "Passage Verdeau", + "amenity": "restaurant", + "cuisine": "korean", + "diet:halal": "yes", + "diet:kosher": "yes", + "diet:vegetarian": "yes", + "fixme": "Emplacement exact", + "name": "Keopi", + "opening_hours": "Tu-Su 10:00-20:00", + "phone": "+33 1 47 70 86 24", + "website": "https://keopi-paris.fr/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.342322, + 48.8736803 + ] + }, + "id": "node/10876238296" + }, + { + "type": "Feature", + "properties": { + "@id": "node/11034414509", + "amenity": "restaurant", + "cuisine": "ramen", + "name": "Menkicchi Ramen" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3426992, + 48.8729311 + ] + }, + "id": "node/11034414509" + }, + { + "type": "Feature", + "properties": { + "@id": "node/11035523081", + "addr:city": "Paris", + "addr:housenumber": "66", + "addr:postcode": "75009", + "addr:street": "Rue du Faubourg Montmartre", + "amenity": "restaurant", + "cuisine": "mexican", + "delivery": "yes", + "name": "El Chingon", + "opening_hours": "Mo-Su 11:30-02:00", + "phone": "+33981636187", + "website": "http://www.elchingon.fr/" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3399715, + 48.8763269 + ] + }, + "id": "node/11035523081" + }, + { + "type": "Feature", + "properties": { + "@id": "node/11063430375", + "addr:housenumber": "48", + "addr:street": "Rue laffitte", + "amenity": "restaurant", + "name": "Juste", + "opening_hours": "Mo 18:30-22:30; Tu-Fr 12:00-14:30, 18:30-22:30; Sa 12:00-15:30, 18:30-23:00", + "phone": "0982339347", + "website": "https://juste-producteur.com" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3385009, + 48.8750353 + ] + }, + "id": "node/11063430375" + }, + { + "type": "Feature", + "properties": { + "@id": "node/11090136337", + "amenity": "restaurant", + "changing_table": "no", + "cuisine": "Chinese; sichuan", + "delivery": "yes", + "level": "0", + "name": "Liziba Chongqing Chicken Pot", + "outdoor_seating": "no", + "smoking": "no", + "takeaway": "yes", + "toilets": "yes", + "toilets:access": "customers", + "toilets:disposal": "flush", + "toilets:position": "seated", + "toilets:wheelchair": "no", + "wheelchair": "no" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3455585, + 48.8740938 + ] + }, + "id": "node/11090136337" + }, + { + "type": "Feature", + "properties": { + "@id": "node/11102867827", + "amenity": "restaurant", + "name": "La Cantine Marocaine" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3456393, + 48.8739711 + ] + }, + "id": "node/11102867827" + }, + { + "type": "Feature", + "properties": { + "@id": "node/11114080053", + "amenity": "restaurant", + "cuisine": "latino", + "name": "Mi Ranchito Paisa", + "opening_hours": "Mo-Fr 12:00-14:30,19:00-23:30; Sa,Su 12:00-23:30", + "phone": "+33 1 48 78 45 94", + "ref:FR:SIREN": "445260193", + "ref:FR:SIRET": "44526019300013", + "website": "https://miranchitopaisa.com" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3448229, + 48.8764947 + ] + }, + "id": "node/11114080053" + }, + { + "type": "Feature", + "properties": { + "@id": "node/11114080055", + "amenity": "restaurant", + "cuisine": "thai", + "name": "Thaï Thaï Gourmand" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3444499, + 48.8770926 + ] + }, + "id": "node/11114080055" + }, + { + "type": "Feature", + "properties": { + "@id": "node/11114080058", + "amenity": "restaurant", + "description": "Restaurant in the hotel La Fantaisie.", + "name": "Golden Poppy", + "outdoor_seating": "yes", + "website": "https://goldenpoppy.com" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3434648, + 48.8753633 + ] + }, + "id": "node/11114080058" + }, + { + "type": "Feature", + "properties": { + "@id": "node/11171620994", + "addr:housenumber": "43", + "addr:street": "Rue Laffite", + "amenity": "restaurant", + "description": "Cantine Kabyle", + "name": "Majoja", + "opening_hours": "Tu-Fr 12:00-15:00; Sa 12:00-16:30", + "phone": "0951492518", + "website": "www.majoujaparis.fr" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 2.3384573, + 48.8754385 + ] + }, + "id": "node/11171620994" + } + ] +} \ No newline at end of file diff --git a/NearestNeighbors/figures/exec_time.png b/NearestNeighbors/figures/exec_time.png new file mode 100644 index 0000000..136f6e5 Binary files /dev/null and b/NearestNeighbors/figures/exec_time.png differ diff --git a/NearestNeighbors/generate_circuit.py b/NearestNeighbors/generate_circuit.py new file mode 100644 index 0000000..24de5a9 --- /dev/null +++ b/NearestNeighbors/generate_circuit.py @@ -0,0 +1,67 @@ +import numpy as np +from concrete import fhe +import config +import network + +_, point_coordinates = network.get() + +N_PTS = point_coordinates.shape[0] + + +points = fhe.LookupTable(point_coordinates.flatten()) + + +def get_point(index): + return (points[2 * index], points[2 * index + 1]) + + +def all_distances(x, y): + xs = np.arange(0, 2 * N_PTS, 2) + ys = np.arange(1, 2 * N_PTS, 2) + a = abs(points[xs] - x) + b = abs(points[ys] - y) + return a + b + + +# TLUs +relu = fhe.univariate(lambda x: x if x > 0 else 0) +is_positive = fhe.univariate(lambda x: 1 if x > 0 else 0) +odd_halving = fhe.univariate(lambda x: (x - 1) // 2 if x % 2 else 0) + + +def swap(this_idx, this_dist, that_idx, that_dist): + """ + Swaps this and that if this > that. + We must pass both the index and the distance for both this and that. + + Returns: + idxmin, min, idxmax, max of this and that based on distance + """ + diff = this_dist - that_dist + idx = odd_halving(2 * (this_idx - that_idx) + is_positive(diff)) + dist = relu(diff) + + idx_min = this_idx - idx + idx_max = that_idx + idx + dist_min = this_dist - dist + dist_max = that_dist + dist + return fhe.array([idx_min, dist_min, idx_max, dist_max]) + + +@fhe.compiler({"x": "encrypted", "y": "encrypted"}) +def knn(x, y): + dist = all_distances(x, y) + idx = list(range(N_PTS)) + for k in range(config.number_of_neighbors): + for i in range(k + 1, N_PTS): + idx[k], dist[k], idx[i], dist[i] = swap(idx[k], dist[k], idx[i], dist[i]) + return fhe.array([get_point(idx[j]) for j in range(config.number_of_neighbors)]) + + +inputset = [(1550, 4289), (1908, 3972), (1705, 4253), (1980, 4071), (1390, 4305), + (1236, 3901), (1469, 4108), (1474, 3842), (1325, 3542), (1643, 4879)] + + +circuit = knn.compile(inputset) + +circuit.server.save(config.circuit_filepath) diff --git a/NearestNeighbors/knn.ipynb b/NearestNeighbors/knn.ipynb new file mode 100644 index 0000000..c90ac3f --- /dev/null +++ b/NearestNeighbors/knn.ipynb @@ -0,0 +1,349 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Nearest Neighbors Example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Server's Data Setup\n", + "The server owns coordinates to points of interest like restaurants and commerces. The coordinates are kept in a LookupTable" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/riad/envs/zama/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "from concrete import fhe\n", + "import numpy\n", + "\n", + "\n", + "# Database of Points of Interests\n", + "points_array = numpy.array([\n", + " [2, 3], [1, 5], [3, 2], [5, 2], [1, 1],\n", + " [9, 4], [13, 2], [14, 13], [9, 8], [8, 0],\n", + " [2, 10], [3, 8], [8, 12], [4, 10], [7, 7],\n", + "])\n", + "N_PTS = points_array.shape[0]\n", + "points = fhe.LookupTable(points_array.flatten())\n", + "\n", + "\n", + "def get_point(index):\n", + " return (points[2*index], points[2*index + 1])\n", + "\n", + "\n", + "def all_distances(x, y):\n", + " xs = numpy.arange(0, 2 * N_PTS, 2)\n", + " ys = numpy.arange(1, 2 * N_PTS, 2)\n", + " a = abs(points[xs] - x)\n", + " b = abs(points[ys] - y)\n", + " return a + b" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use swap sort to find the $K$ nearest points to a given point. However, we are interested in the indices of the elements, not just their distances. We must therefore work on tuples of index and distance, effectively implementing numpy argpartition." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# TLUs\n", + "relu = fhe.univariate(lambda x: x if x > 0 else 0)\n", + "is_positive = fhe.univariate(lambda x: 1 if x > 0 else 0)\n", + "odd_halving = fhe.univariate(lambda x: (x-1)//2 if x % 2 else 0) " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def swap(this_idx, this_dist, that_idx, that_dist):\n", + " \"\"\"\n", + " Swaps this and that if this > that. \n", + " We must pass both the index and the distance for both this and that.\n", + "\n", + " Returns:\n", + " idxmin, min, idxmax, max of this and that based on distance\n", + " \"\"\"\n", + " diff = this_dist - that_dist\n", + " idx = odd_halving(2 * (this_idx - that_idx) + is_positive(diff))\n", + " dist = relu(diff)\n", + "\n", + " idx_min = this_idx - idx\n", + " idx_max = that_idx + idx \n", + " dist_min = this_dist - dist\n", + " dist_max = that_dist + dist\n", + " return fhe.array([idx_min, dist_min, idx_max, dist_max])\n", + "\n", + "\n", + "@fhe.compiler({\"x\": \"encrypted\", \"y\": \"encrypted\"})\n", + "def knn(x, y):\n", + " dist = all_distances(x, y)\n", + " idx = list(range(N_PTS))\n", + " for k in range(2):\n", + " for i in range(k+1, N_PTS):\n", + " idx[k], dist[k], idx[i], dist[i] = swap(idx[k], dist[k], idx[i], dist[i])\n", + " return fhe.array([get_point(idx[j]) for j in range(2)])\n", + "\n", + "\n", + "inputset = [(4, 3), (0, 0), (15, 3), (4, 15), (9, 4), (13, 2), (14, 13), (9, 8), (8, 0), (2, 10), (3, 8), (8, 12), (4, 10), (7, 7)]\n", + "\n", + "circuit = knn.compile(inputset)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Client\n", + "The client simply invokes the server's nearest neighbors circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4.8 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit -r 1 -n 1\n", + "circuit.client.keys.generate()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def nearest(x, y):\n", + " ex, ey = circuit.encrypt(x, y)\n", + " res = circuit.run(ex, ey) # Simulate request to the server\n", + " return circuit.decrypt(res)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Benchmarks" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "9.08 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit -r 1 -n 1\n", + "nearest(4, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note: Extended benchmark results can be found in README.MD" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Unitary tests" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Suppose we have two values, this = 50 and that = 20, the swap function should swap the values and the indexes:\n", + "New index of this is 10\n", + "New value of this is 20\n", + "New index of that is 0\n", + "New value of that is 50\n", + "Now if we have , this = 5 and that = 20, the swap function should keep everything unchanged:\n", + "New index of this is 0\n", + "New value of this is 5\n", + "New index of that is 10\n", + "New value of that is 20\n" + ] + } + ], + "source": [ + "# unitary test of swap function\n", + "print(\"Suppose we have two values, this = 50 and that = 20, the swap function should swap the values and the indexes:\")\n", + "this_idx = 0\n", + "this_dist = 50\n", + "that_idx = 10\n", + "that_dist = 20\n", + "this_idx, this_dist, that_idx, that_dist = swap(this_idx, this_dist, that_idx, that_dist)\n", + "\n", + "print(f\"New index of this is {this_idx}\")\n", + "print(f\"New value of this is {this_dist}\")\n", + "\n", + "print(f\"New index of that is {that_idx}\")\n", + "print(f\"New value of that is {that_dist}\")\n", + "\n", + "\n", + "\n", + "print(\"Now if we have , this = 5 and that = 20, the swap function should keep everything unchanged:\")\n", + "this_idx = 0\n", + "this_dist = 5\n", + "that_idx = 10\n", + "that_dist = 20\n", + "this_idx, this_dist, that_idx, that_dist = swap(this_idx, this_dist, that_idx, that_dist)\n", + "\n", + "print(f\"New index of this is {this_idx}\")\n", + "print(f\"New value of this is {this_dist}\")\n", + "\n", + "print(f\"New index of that is {that_idx}\")\n", + "print(f\"New value of that is {that_dist}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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yJAh6b7m9evX2bp1q90+T91mx44dCQwMZMiQIQQEBLB3716WLVtm3eaPP/5Iu3btqFChAmPGjOHKlStMmzaNJk2aEB0dneGJPCv769y5c7Rr144nn3ySnj17UqpUKVJSUnjkkUfYuHEjzz77LNWqVWP37t18+OGHHDhwgMWLF1vXf+aZZ5g7dy7du3encePG/PTTT3To0CHd2OrWrZupzoELFizg8uXLDBw4kOLFixMVFcW0adM4fvw4CxYssKmbmWPeMAw6derExo0bef7556lWrRqLFi2iT58+t43lwQcf5KWXXmLq1Kn83//9H9WqVQOw/gvw559/0rVrV/r370+fPn349NNP6du3L/Xq1aN69eqApbWrefPmnDhxgueee46yZcvy66+/MnLkSOLi4jJ16eTxxx+nUqVKvPvuuxleort48SLNmjVj7969PP3009StW5e///6bJUuWcPz4ce65554018vscff888/z7bffMnjwYEJDQzl37hwbN25k79691K1bF7AkRO3ataNevXpERETg4uLC7Nmzeeihh9iwYQMNGzbM1LbeeOMNEhISOH78OB9++CFApjrEbty4kYULF/LCCy/g4+PD1KlTeeyxxzh69CjFixcH4PTp0zzwwAPWL9ISJUqwcuVK+vfvT2JiIkOHDk13+8uXL+eJJ56gZs2ajBs3jvPnz9O/f39Kly6dZv3bnYdSmc1mwsLCeOCBB5gwYQKrVq0iIiKCGzdu8NZbbwGW4/mRRx5h3bp19O/fnzp16rB69WpeffVVTpw4Yd1PqX766Se++eYbBg8ezD333EP58uUddm5LT1oxnjx5koYNG3LhwgWeffZZqlatyokTJ/j222+5fPky7u7u6W4rM8fe7t27efjhhylRogRjxozhxo0bREREpPv9V69ePQzD4Ndff82eDv5GATd79mwDMLZu3Wot69OnjwEYb731lk3d++67z6hXr571eWxsrAEYXl5exvHjx63lW7ZsMQDj5ZdftpY1b97caN68ud3r9+nTxyhXrpz1+dmzZw3AiIiIyFL89erVM65du2YtnzBhggEY33//vWEYhnHmzBnD3d3dePjhhw2z2WytN336dAMwPv3003RjSn2fxYsXN+Lj463l33//vQEYS5cutZYNGjTISOuwGjJkiOHr62vcuHEjU+8r1Z9//mkAxrRp02zK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DRo0oGfPntmyL+rVqwdYOq62bdsWV1dXnnzySZ555hni4+N56KGHKFOmDEeOHGHatGnUqVPntll5p06deOONN0hMTMTX1xewfDgjIyMJDw+nTp069OvXj8DAQPbt28cff/xh/QC9//77tGvXjkaNGtG/f3/rMFg/P78M5+fJjv3Vq1cvvvnmG55//nnWrVtHkyZNMJvN7Nu3j2+++YbVq1dTv3596tSpw1NPPcXHH39MQkICjRs3Zu3aten+It2+fTvx8fF06tQpw9evWrUqFStW5JVXXuHEiRP4+vry3Xff3VVTdnh4OE2aNOH111/n8OHDhIaGsnDhwkx/OdapUwdXV1fGjx9PQkICHh4ePPTQQ5QsWTLTMbz66qssWbKEjh07WodmX7p0id27d/Ptt99y+PDhbGuyfvXVV/n22295/PHHefrpp6lXrx7x8fEsWbKEGTNmULt27TTXy8xxl5SURJkyZejatSu1a9fG29ubH3/8ka1bt/LBBx8AluP8k08+oV27dlSvXp1+/fpRunRpTpw4wbp16/D19WXp0qWZ2hZYPp9ff/01w4YNo0GDBnh7e2fLRJPvvfce69at4/7772fAgAGEhoYSHx9PdHQ0P/74Y4bzhbz77rt06tSJJk2a0K9fP86fP8/06dOpUaNGml+gmeXp6cmqVavo06cP999/PytXrmT58uX83//9n7UvWHh4OC1btuSNN97g8OHD1K5dmx9++IHvv/+eoUOH2vVbS4ujzm1Z8e677/LDDz/QvHlz61QQcXFxLFiwgI0bN9p1Kk6NIzPHHljmPVu1ahXNmjXjhRde4MaNG0ybNo3q1avz+++/2217zZo1NGnSxHp57a5ly9gnJ5be8OsiRYrY1b11uFvqcMD333/f+OCDD4zg4GDDw8PDaNasmbFr1y679efOnWtUqFDBcHd3N+rUqWOsXr3abqizYRjGr7/+atSrV89wd3e/7VDs1Ph//vln49lnnzX8/f0Nb29vo0ePHjZDP1NNnz7dqFq1qlGoUCGjVKlSxsCBA+2G+KU3/Pr999+3296t8d24ccN48cUXjRIlShgmk8m6v7799lvj4YcfNkqWLGm4u7sbZcuWNZ577jkjLi4u3feW6vTp04abm5vxxRdf2C3buHGj0aZNG8PHx8coUqSIUatWLbvhjD/++KPRpEkTw8vLy/D19TXC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Rjo5c4pBVGw8Z5KSKenjScOQYri6mBwdlsNpv4iIFAwOTWR++eUX3n//fbZv305cXByLFi2ic+fO1uWGYRAREcGsWbO4cOECTZo0ITIykkqVKjku6Dxk1Z44xi6NIS4h2VoW6OdJRHgoYTUCHRiZY2m/iIgUHA69tHTp0iVq167NRx99lObyCRMmMHXqVGbMmMGWLVsoUqQIbdu2JTk5Oc36BcmqPXEMnBtt82UNcCohmYFzo1m1J85BkTmW9ouISMFiMgzDcHQQACaTyaZFxjAMgoKCGD58OK+88goACQkJlCpVijlz5vDkk09maruJiYn4+fmRkJCAr69vToWfq8wpBk3H/2T3ZZ3KBAT4ebJxxEMF6nKK9ouISP6R2e/vPNvZNzY2llOnTtG6dWtrmZ+fH/fffz+bN29Od72rV6+SmJho88hvomLj0/2yBjCAuIRkomLjcy+oPED7RUSk4MmzicypU6cAKFWqlE15qVKlrMvSMm7cOPz8/KyP4ODgHI3TEc4kZe7SWmbr5RfaLyIiBU+eTWTu1MiRI0lISLA+jh075uiQsl1JH89srZdfaL+IiBQ8eTaRCQgIAOD06dM25adPn7YuS4uHhwe+vr42j/ymYUgxAv08Sa+XhwnLKJ2GIcVyMyyH034RESl48mwiExISQkBAAGvXrrWWJSYmsmXLFho1auTAyBzP1cVERHgogN2XdurziPDQAtehVftFRKTgcWgic/HiRXbu3MnOnTsBSwffnTt3cvToUUwmE0OHDuU///kPS5YsYffu3fTu3ZugoCCbuWYKqrAagUT2rEuAn+1lkgA/TyJ71i2w86Vov4iIFCwOHX69fv16WrZsaVfep08f5syZY50Qb+bMmVy4cIGmTZvy8ccfU7ly5Uy/Rn4cfn0zzWCbNu0XERHnltnv7zwzj0xOye+JjIiISH7k9PPIiIiIiNyOEhkRERFxWkpkRERExGkpkRERERGnpURGREREnJYSGREREXFaSmRERETEaSmREREREaelREZERESclhIZERERcVpKZERERMRpKZERERERp6VERkRERJyWEhkRERFxWkpkRERExGkpkRERERGnpURGREREnJYSGREREXFaSmRERETEaSmREREREaelREZERESclhIZERERcVpKZERERMRpKZERERERp6VERkRERJyWEhkRERFxWkpk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snz58uyNTkRERCQDWU5kypQpQ1xcHAAVK1bkhx9+AGDr1q14eHhkb3QiIiIiGchyIvPoo4+ydu1aAF588UVGjRpFpUqV6N27N08//XS2BygiIiKSnrueR2bz5s1s3ryZSpUqER4enl1xZRvNIyMiIuJ8cm0emUaNGjFs2LAcS2JOnDhBz549KV68OF5eXtSsWZNt27blyGuJiIiIc7mrRMbX15e//voru2Kxc/78eZo0aUKhQoVYuXIlMTExfPDBB/j7++fYa4qIiIjzyPSEeCdPniQoKMimLBvvbpCm8ePHExwczOzZs61lISEhOfqaIiIi4jwy3SJTvXp15s2bl5Ox2FmyZAn169fn8ccfp2TJktx3333MmjUrw3WuXr1KYmKizUNERETyp0wnMu+88w7PPfccjz/+uPU2BD179szRDrR//fUXkZGRVKpUidWrVzNw4EBeeuklPvvss3TXGTduHH5+ftZHcHBwjsUnIiIijpWlUUuxsbH079+fmJgYZs2aleOjlNzd3alfvz6//vqrteyll15i69atbN68Oc11rl69ytWrV63PExMTCQ4O1qglERERJ5LZUUtZumlkSEgIP/30E9OnT6dLly5Uq1YNNzfbTURHR99ZxGkIDAwkNDTUpqxatWp899136a7j4eGhiflEREQKiCzf/frIkSMsXLgQf39/OnXqZJfIZKcmTZqwf/9+m7IDBw5Qrly5HHtNERERcR5ZykJSbxbZunVr/vjjD0qUKJFTcQHw8ssv07hxY9599126detGVFQUM2fOZObMmTn6uiIiIuIcMt1HJiwsjKioKCZPnkzv3r1zOi6rZcuWMXLkSA4ePEhISAjDhg1jwIABmV5fM/uKiIg4n2zvI2M2m/n9998pU6ZMtgSYWR07dqRjx465+poiIiLiHDKdyKxZsyYn4xARERHJsru+15KIiIiIoyiREREREaelREZERESclhIZERERcVpKZERERMRpKZERERERp6VERkRERJyWEhkRERFxWkpkRERExGkpkRERERGnpURGREREnJYSGREREXFaSmRERETEaWX67tfyL3OKQVRsPGeSkinp40nDkGK4upgcHZaIiEiBo0Qmi1btiWPs0hjiEpKtZYF+nkSEhxJWI9CBkYmIiBQ8urSUBav2xDFwbrRNEgNwKiGZgXOjWbUnzkGRiYiIFExKZDLJnGIwdmkMRhrLUsvGLo3BnJJWDREREckJSmQyKSo23q4l5mYGEJeQTFRsfO4FJSIiUsApkcmkM0npJzF3Uk9ERETunhKZTCrp45mt9UREROTuKZHJpIYhxQj08yS9QdYmLKOXGoYUy82wRERECjQlMpnk6mIiIjwUwC6ZSX0eER6q+WRERERykRKZLAirEUhkz7oE+NlePgrw8ySyZ13NIyMiIpLLNCFeFoXVCKRNaIBm9hUREckDlMjcAVcXE40qFnd0GCIiIgWeLi2JiIiI01IiIyIiIk5LiYyIiIg4LSUyIiIi4rSUyIiIiIjTUiIjIiIiTkuJjIiIiDgtJTIiIiLitJTIiIiIiNNSIiMiIiJOS4mMiIiIOC0lMiIiIuK0lMiIiIiI01IiIyIiIk7LqRKZ9957D5PJxNChQx0diohItjKnGGw+dI7vd55g86FzmFMMR4ck4hTcHB1AZm3dupX//ve/1KpVy9GhiIhkq1V74hi7NIa4hGRrWaCfJxHhoYTVCHRgZCJ5n1O0yFy8eJEePXowa9Ys/P39HR2OiEi2WbUnjoFzo22SGIBTCckMnBvNqj1xDopMxDk4RSIzaNAgOnToQOvWrR0diohItjGnGIxdGkNaF5FSy8YujdFlJpEM5PlLS/Pnzyc6OpqtW7dmqv7Vq1e5evWq9XliYmJOhSYicleiYuPtWmJuZgBxCclExcbTqGLx3AtMxInk6RaZY8eOMWTIEL788ks8PT0ztc64cePw8/OzPoKDg3M4ShGRO3MmKf0k5k7qiRREeTqR2b59O2fOnKFu3bq4ubnh5ubGzz//zNSpU3Fzc8NsNtutM3LkSBISEqyPY8eOOSByEZHbK+mTuR9oma0nUhDl6UtLrVq1Yvfu3TZl/fr1o2rVqowYMQJXV1e7dTw8PPDw8MitEEVE7ljDkGIE+nlyKiE5zX4yJiDAz5OGIcVyOzQRp5GnExkfHx9q1KhhU1akSBGKFy9uVy4i4mxcXUxEhIcycG40JrBJZkz//BsRHoqriymNtUUE8vilJRGR/C6sRiCRPesS4Gd7+SjAz5PInnU1j4zIbZgMw8jX4/oSExPx8/MjISEBX19fR4cjIpImc4pBVGw8Z5KSKeljuZyklhgpyDL7/Z2nLy2JiBQUri4mDbEWuQO6tCQiIiJOS4mMiIiIOC0lMiIiIuK0lMiIiIiI01IiIyIiIk5LiYyIiIg4LSUyIiIi4rSUyIiIiIjTUiIjIiIiTkuJjIiIiDgtJTIiIiLitJTIiIiIiNNSIiMiIiJOS3e/dnLmFIOo2HjOJCVT0seThiHFcHUxOTosERGRXKFExomt2hPH2KUxxCUkW8sC/TyJCA8lrEagAyMTERHJHbq05KRW7Ylj4NxomyQG4FRCMgPnRrNqT5yDIhMREck9SmSckDnFYOzSGIw0lqWWjV0agzklrRoiIiL5hxIZJxQVG2/XEnMzA4hLSCYqNj73ghIREXEAJTJO6ExS+knMndQTERFxVkpknFBJH89srSciIuKslMg4oYYhxQj08yS9QdYmLKOXGoYUy82wREREcp0SGSfk6mIiIjwUwC6ZSX0eER6q+WRERCTfUyLjpMJqBBLZsy4BfraXjwL8PInsWVfzyIiISIGgCfGcWFiNQNqEBmhmXxERKbCUyDg5VxcTjSoWd3QYIiIiDqFLSyIiIuK0lMiIiIiI01IiIyIiIk5LiYyIiIg4LSUyIiIi4rSUyIiIiIjTUiIjIiIiTkuJjIiIiDgtTYgn2ePqVfjjDzh7FlxdoWJFKF8eTJplWEREco4SGblz16/D4sUQGQkbN1qe36xYMejaFQYNglq1HBKiiIjkb7q0JHdm1y5o0AC6dYMbN2DSJNi8GY4cgUOHYPlyeP55WLYMateGF16AixcdHbWIiOQzJsMwDEcHkZMSExPx8/MjISEBX19fR4eTP3z7LXTvDlWqwP/+Bw0bpl/3+nWYMQNefx3KlYM1a6B06dyLVUREnFJmv7/VIiNZ88MP8OSTlktG27ZlnMQAFCoEL74I0dGWFpnWrSExMXdiFRGRfE+JjGTe+fPQt68lGfn8c/DwyPy6VarAjz/C8ePw6qs5FqKIiBQs6uwrmTd2LFy+bLmc5GZ/6JhTDKJi4zmTlExJH08ahhTD1eWmUUuVK8P778PAgfDss1CvXi4G7xi33SciIlmk84qtPN1HZty4cSxcuJB9+/bh5eVF48aNGT9+PFWqVMn0NtRHJptcvGjp2zJ4MLzzjt3iVXviGLs0hriEZGtZoJ8nEeGhhNUI/Lei2WwZmt2yJcyenRuRO0ym94mISCYVpPNKvugj8/PPPzNo0CB+++031qxZw/Xr13n44Ye5dOmSo0MreFatsvRtGTDAftGeOAbOjbb5YAGcSkhm4NxoVu2J+7fQ1RX694cFCyxJTT6VpX0iIpIJOq+kLU8nMqtWraJv375Ur16d2rVrM2fOHI4ePcr27dsdHVrBs22bpUWmfHmbYnOKwdilMaTVrJdaNnZpDOaUm2o0aQKXLsH+/TkVrUPd0T4REcmAzivpy9OJzK0SEhIAKFasWLp1rl69SmJios1DssGff0JoqF1xVGy83a+DmxlAXEIyUbHx/xambufgwWwOMm+4o30iIpIBnVfS5zSJTEpKCkOHDqVJkybUqFEj3Xrjxo3Dz8/P+ggODs7FKPOxGzfA3d2u+ExS+h+sdOulbufGjeyILM+5o30iIpIBnVfS5zSJzKBBg9izZw/z58/PsN7IkSNJSEiwPo4dO5ZLEeZzxYpBnP3115I+npla3abeqVOWf4sXz47I8pw72iciIhnQeSV9TpHIDB48mGXLlrFu3TrKlCmTYV0PDw98fX1tHpIN6taFPXssN4e8ScOQYgT6eZLewD8Tlh71DUNuuhyY2sepTp2ciNTh7mifiIhkQOeV9OXpRMYwDAYPHsyiRYv46aefCAkJcXRIBVfLlnDtGixdalPs6mIiItzS5+XWD1jq84jwUNs5Dr791pIYFS2aY+E60h3tExGRDOi8kr48ncgMGjSIuXPnMm/ePHx8fDh16hSnTp3iypUrjg6t4KleHZo2hQ8/hFumHgqrEUhkz7oE+Nk2aQb4eRLZs67t3AYHDlhuJPn887kRtcNkaZ+IiGSCzitpy9MT4plMaWeWs2fPpm/fvpnahibEy0Zr1sDDD8N//2uZmfcWt51tMiUFWrSAEydg924oXDj3YncQzcApItmtoJxXMvv9nadvUZCHc6yCqU0by4R4Q4dC1arw4IM2i11dTDSqmE4HXsOAYcNg40ZYt65AJDFwm30iInIHdF6xlacvLUkeNHUqNG4MYWGWey5lJtk8fx569IApU+Djj6F585yPU0RECgQlMpI1np6WDr9PPgnPPAMPPWR5ntbtBs6dg4kTLf1rVqyAefPyfd8YERHJXXn60pLkUV5e8Omn8PjjEBEBjzwCRYrAffdBUJBlort9+2DvXihUCJ54AsaNs9ziQEREJBvl6c6+2UGdfXPB9u3w008QHQ1nz1puDHnvvVCvHoSHQ4kSjo5QREScTL7o7CtOol49y0NERCSXqY+MiIiIOC0lMiIiIuK0lMiIiIiI01Ifmay6dAl27oQ//oArV8DbG2rVgpo1LUOTRUREJNcokcmsbdssE7p9843l5okuLpbE5coVy6RwRYpAz54wZAhUq+boaEVERAoEXVq6ncuXLclJw4aweTP85z+WYcZXrlhaZy5fhi1b4JVXLBPD1aoFb70F1687OnIREZF8T/PIZCQhAdq1gx074N134aWXLHOkpOfqVUuiM26cZb1vvwUPj7t7AyIiIgVQZr+/1SKTnpQU6NLFMjvtL7/Ayy9nnMSAJWl5+23LdPw//ghPP507sYqIiBRQSmTSM3WqZbbahQuhQYOsrfvww5YbKs6bBwsW5Ex8IiIioktLaUpIgDJloG9fmDbNbrE5xSAqNp4zScmU9PGkYUgxXF1MtpUMAx57DLZuhdhYcMuhftVmM2zYAHFxEBgIzZrdvuVIREQkj9MtCu7GF19YOvOOHGm3aNWeOMYujSEuIdlaFujnSUR4KGE1Av+taDLBm29apu5fuhQefTT741y40NIR+fjxf8vKlLGMrurSJftfT0REJI/RpaW0fP89tG1ruZPzTVbtiWPg3GibJAbgVEIyA+dGs2pPnO126taF2rUt28tuCxdC1662SQzAiROW8oULs/81RURE8hglMrcyDMvdnB94wKbYnGIwdmkMaV2HSy0buzQGc8otNe6/37K97GQ2W1pi0roqmFo2dKilnoiISD6mROZWFy/C+fNQqZJNcVRsvF1LzM0MIC4hmajYeNsFlSrBkSPZG+OGDfYtMTbBGHDsmKWeiIhIPqZE5lapLRoutrvmTFL6SUyG9Vxd0245uRtxcbevk5V6IiIiTkqJzK28vS23G7ilFaWkT+buo2RX7/BhCAjIpuD+ERh4+zpZqSciIuKklMjcysUF7rvPMmz6Jg1DihHo54kpndVMWEYvNQwpZrtg61aoXz97Y2zWzDI6yZRONCYTBAdb6omIiORjSmTS0rYtLFsGFy5Yi1xdTESEhwLYJTOpzyPCQ23nkzl0yHJ/prZtszc+V1fLEGuwT2ZSn0+erPlkREQk31Mik5ZnnoEbN2D6dJvisBqBRPasS4Cf7eWjAD9PInvWtZ1HBmDiRPD3hyeeyP4Yu3Sx3MupdGnb8jJlLOWaR0ZERAoAzeybntdes7R6REdD9eo2izI1s+/69dCypaVlZMiQu34f6dLMviIikg9l9vtbiUx6kpMts/JeuQLr1kG5cplfd88eSxJTvbrlfk0uavgSERHJCt39+m55esLKlZa/H3gAli+//TqGYbm9QdOmlks8CxcqiREREclB+pbNSNmysGkT1KkDHTtCWJjldgNJSbb1zp+Hr76yJDC9e1vqrlsHxYqluVkRERHJHrpp5O0EBsKKFfD11zBpEnTubBkZVKEC+PhYRjYdPmyp27KlpeWmfXsHBiwiIlJwqI9MVsXEwJYt8McfcPmyZQK92rUtl58qVrz77YuIiEimv7/VIpNVoaGWh4iIiDic+siIiIiI01IiIyIiIk5LiYyIiIg4LSUyIiIi4rSUyIiIiIjTUiIjIiIiTkuJjIiIiDgtJTIiIiLitJTIiIiIiNPSzL4iUmCZUwyiYuM5k5RMSR9PGoYUw9XF5OiwRCQLnCKR+eijj3j//fc5deoUtWvXZtq0aTRs2NDRYYmIE1u1J46xS2OIS0i2lgX6eRIRHkpYjUAHRiYiWZHnLy19/fXXDBs2jIiICKKjo6lduzZt27blzJkzjg5NRJzUqj1xDJwbbZPEAJxKSGbg3GhW7YlzUGQiklV5PpGZNGkSAwYMoF+/foSGhjJjxgwKFy7Mp59+6ujQRMQJmVMMxi6NwUhjWWrZ2KUxmFPSqiEieU2eTmSuXbvG9u3bad26tbXMxcWF1q1bs3nz5jTXuXr1KomJiTYPEZFUUbHxdi0xNzOAuIRkomLjcy8oEbljeTqR+fvvvzGbzZQqVcqmvFSpUpw6dSrNdcaNG4efn5/1ERwcnBuhioiTOJOUfhJzJ/VExLHydCJzJ0aOHElCQoL1cezYMUeHJCJ5SEkfz2ytJyKOladHLd1zzz24urpy+vRpm/LTp08TEBCQ5joeHh54eHjkRngi4oQahhQj0M+TUwnJafaTMQEBfpah2CKS9+XpFhl3d3fq1avH2rVrrWUpKSmsXbuWRo0aOTAyEXFWri4mIsJDAUvScrPU5xHhoZpPRsRJ5OlEBmDYsGHMmjWLzz77jL179zJw4EAuXbpEv379HB2aiDipsBqBRPasS4Cf7eWjAD9PInvW1TwyIk4kT19aAnjiiSc4e/Yso0eP5tSpU9SpU4dVq1bZdQAWEcmKsBqBtAkN0My+Ik7OZBhGvp4sITExET8/PxISEvD19XV0OCIiIpIJmf3+zvOXlkRERETSo0RGREREnJYSGREREXFaSmRERETEaSmREREREaelREZERESclhIZERERcVpKZERERMRpKZERERERp5Xnb1Fwt1InLk5MTHRwJCIiIpJZqd/bt7sBQb5PZJKSkgAIDg52cCQiIiKSVUlJSfj5+aW7PN/fayklJYWTJ0/i4+ODyZQ/bwaXmJhIcHAwx44d0/2kbqL9Yk/7xJ72Sdq0X+xpn9jLyX1iGAZJSUkEBQXh4pJ+T5h83yLj4uJCmTJlHB1GrvD19dWHKw3aL/a0T+xpn6RN+8We9om9nNonGbXEpFJnXxEREXFaSmRERETEaSmRyQc8PDyIiIjAw8PD0aHkKdov9rRP7GmfpE37xZ72ib28sE/yfWdfERERyb/UIiMiIiJOS4mMiIiIOC0lMiIiIuK0lMiIiIiI01Ii48TGjRtHgwYN8PHxoWTJknTu3Jn9+/c7Oqw85b333sNkMjF06FBHh+JQJ06coGfPnhQvXhwvLy9q1qzJtm3bHB2WQ5nNZkaNGkVISAheXl5UrFiRt99++7b3dclPfvnlF8LDwwkKCsJkMrF48WKb5YZhMHr0aAIDA/Hy8qJ169YcPHjQMcHmooz2y/Xr1xkxYgQ1a9akSJEiBAUF0bt3b06ePOm4gHPB7Y6Vmz3//POYTCYmT56cK7EpkXFiP//8M4MGDeK3335jzZo1XL9+nYcffphLly45OrQ8YevWrfz3v/+lVq1ajg7Foc6fP0+TJk0oVKgQK1euJCYmhg8++AB/f39Hh+ZQ48ePJzIykunTp7N3717Gjx/PhAkTmDZtmqNDyzWXLl2idu3afPTRR2kunzBhAlOnTmXGjBls2bKFIkWK0LZtW5KTk3M50tyV0X65fPky0dHRjBo1iujoaBYuXMj+/ft55JFHHBBp7rndsZJq0aJF/PbbbwQFBeVSZIAh+caZM2cMwPj5558dHYrDJSUlGZUqVTLWrFljNG/e3BgyZIijQ3KYESNGGE2bNnV0GHlOhw4djKefftqmrEuXLkaPHj0cFJFjAcaiRYusz1NSUoyAgADj/ffft5ZduHDB8PDwML766isHROgYt+6XtERFRRmAceTIkdwJysHS2yfHjx83SpcubezZs8coV66c8eGHH+ZKPGqRyUcSEhIAKFasmIMjcbxBgwbRoUMHWrdu7ehQHG7JkiXUr1+fxx9/nJIlS3Lfffcxa9YsR4flcI0bN2bt2rUcOHAAgF27drFx40batWvn4MjyhtjYWE6dOmXzGfLz8+P+++9n8+bNDows70lISMBkMlG0aFFHh+IwKSkp9OrVi1dffZXq1avn6mvn+5tGFhQpKSkMHTqUJk2aUKNGDUeH41Dz588nOjqarVu3OjqUPOGvv/4iMjKSYcOG8X//939s3bqVl156CXd3d/r06ePo8Bzm9ddfJzExkapVq+Lq6orZbOadd96hR48ejg4tTzh16hQApUqVsikvVaqUdZlAcnIyI0aM4KmnnirQN5IcP348bm5uvPTSS7n+2kpk8olBgwaxZ88eNm7c6OhQHOrYsWMMGTKENWvW4Onp6ehw8oSUlBTq16/Pu+++C8B9993Hnj17mDFjRoFOZL755hu+/PJL5s2bR/Xq1dm5cydDhw4lKCioQO8Xybzr16/TrVs3DMMgMjLS0eE4zPbt25kyZQrR0dGYTKZcf31dWsoHBg8ezLJly1i3bh1lypRxdDgOtX37ds6cOUPdunVxc3PDzc2Nn3/+malTp+Lm5obZbHZ0iLkuMDCQ0NBQm7Jq1apx9OhRB0WUN7z66qu8/vrrPPnkk9SsWZNevXrx8ssvM27cOEeHlicEBAQAcPr0aZvy06dPW5cVZKlJzJEjR1izZk2Bbo3ZsGEDZ86coWzZstbz7pEjRxg+fDjly5fP8ddXi4wTMwyDF198kUWLFrF+/XpCQkIcHZLDtWrVit27d9uU9evXj6pVqzJixAhcXV0dFJnjNGnSxG5Y/oEDByhXrpyDIsobLl++jIuL7W85V1dXUlJSHBRR3hISEkJAQABr166lTp06ACQmJrJlyxYGDhzo2OAcLDWJOXjwIOvWraN48eKODsmhevXqZdcfsW3btvTq1Yt+/frl+OsrkXFigwYNYt68eXz//ff4+PhYr1v7+fnh5eXl4Ogcw8fHx66PUJEiRShevHiB7Tv08ssv07hxY9599126detGVFQUM2fOZObMmY4OzaHCw8N55513KFu2LNWrV2fHjh1MmjSJp59+2tGh5ZqLFy/y559/Wp/Hxsayc+dOihUrRtmyZRk6dCj/+c9/qFSpEiEhIYwaNYqgoCA6d+7suKBzQUb7JTAwkK5duxIdHc2yZcswm83Wc2+xYsVwd3d3VNg56nbHyq3JXKFChQgICKBKlSo5H1yujI2SHAGk+Zg9e7ajQ8tTCvrwa8MwjKVLlxo1atQwPDw8jKpVqxozZ850dEgOl5iYaAwZMsQoW7as4enpaVSoUMF44403jKtXrzo6tFyzbt26NM8hffr0MQzDMgR71KhRRqlSpQwPDw+jVatWxv79+x0bdC7IaL/Exsame+5dt26do0PPMbc7Vm6Vm8OvTYZRgKaxFBERkXxFnX1FRETEaSmREREREaelREZERESclhIZERERcVpKZERERMRpKZERERERp6VERkRERJyWEhkRKRDWr1+PyWTiwoULjg5FRLKREhkRyVVms5nGjRvTpUsXm/KEhASCg4N54403cuR1GzduTFxcHH5+fjmyfRFxDM3sKyK57sCBA9SpU4dZs2bRo0cPAHr37s2uXbvYunVrvr1fjYhkP7XIiEiuq1y5Mu+99x4vvvgicXFxfP/998yfP5/PP/883SRmxIgRVK5cmcKFC1OhQgVGjRrF9evXAcud4Fu3bk3btm1J/W0WHx9PmTJlGD16NGB/aenIkSOEh4fj7+9PkSJFqF69OitWrMj5Ny8i2Up3vxYRh3jxxRdZtGgRvXr1Yvfu3YwePZratWunW9/Hx4c5c+YQFBTE7t27GTBgAD4+Prz22muYTCY+++wzatasydSpUxkyZAjPP/88pUuXtiYytxo0aBDXrl3jl19+oUiRIsTExODt7Z1Tb1dEcoguLYmIw+zbt49q1apRs2ZNoqOjcXPL/G+riRMnMn/+fLZt22YtW7BgAb1792bo0KFMmzaNHTt2UKlSJcDSItOyZUvOnz9P0aJFqVWrFo899hgRERHZ/r5EJPfo0pKIOMynn35K4cKFiY2N5fjx4wA8//zzeHt7Wx+pvv76a5o0aUJAQADe3t68+eabHD161GZ7jz/+OI8++ijvvfceEydOtCYxaXnppZf4z3/+Q5MmTYiIiOD333/PmTcpIjlKiYyIOMSvv/7Khx9+yLJly2jYsCH9+/fHMAzeeustdu7caX0AbN68mR49etC+fXuWLVvGjh07eOONN7h27ZrNNi9fvsz27dtxdXXl4MGDGb7+M888w19//WW9tFW/fn2mTZuWU29XRHKIEhkRyXWXL1+mb9++DBw4kJYtW/K///2PqKgoZsyYQcmSJbn33nutD7AkPeXKleONN96gfv36VKpUiSNHjthtd/jw4bi4uLBy5UqmTp3KTz/9lGEcwcHBPP/88yxcuJDhw4cza9asHHm/IpJzlMiISK4bOXIkhmHw3nvvAVC+fHkmTpzIa6+9xuHDh+3qV6pUiaNHjzJ//nwOHTrE1KlTWbRokU2d5cuX8+mnn/Lll1/Spk0bXn31Vfr06cP58+fTjGHo0KGsXr2a2NhYoqOjWbduHdWqVcv29yoiOUudfUUkV/3888+0atWK9evX07RpU5tlbdu25caNG/z444+YTCabZa+99hqffvopV69epUOHDjzwwAOMGTOGCxcucPbsWWrWrMmQIUMYOXIkANevX6dRo0ZUrFiRr7/+2q6z74svvsjKlSs5fvw4vr6+hIWF8eGHH1K8ePFc2xcicveUyIiIiIjT0qUlERERcVpKZERERMRpKZERERERp6VERkRERJyWEhkRERFxWkpkRERExGkpkRERERGnpURGREREnJYSGREREXFaSmRERETEaSmREREREaelREZERESc1v8DNtL5yjJdp48AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# unitary test of nearest function (which is indirectly testing knn function)\n", + "import matplotlib.pyplot as plt\n", + "\n", + "\n", + "x = [point[0] for point in points_array]\n", + "y = [point[1] for point in points_array]\n", + "\n", + "\n", + "first_location = [2, 1]\n", + "closest_to_first_location = nearest(first_location[0], first_location[1])\n", + "\n", + "second_location = [8,8]\n", + "closest_to_second_location= nearest(second_location[0], second_location[1])\n", + "\n", + "third_location = [12,13]\n", + "closest_to_third_location= nearest(third_location[0], third_location[1])\n", + "\n", + "\n", + "plt.scatter(x, y)\n", + "\n", + "\n", + "plt.scatter(first_location[0], first_location[1], color='red', label='First location')\n", + "plt.scatter(second_location[0], second_location[1], color='green', label='Second location')\n", + "plt.scatter(third_location[0], third_location[1], color='yellow', label='Third location')\n", + "\n", + "for point in closest_to_first_location:\n", + " plt.scatter(point[0], point[1], marker='o', facecolor='none', edgecolor='red', s=200)\n", + "\n", + "for point in closest_to_second_location:\n", + " plt.scatter(point[0], point[1], marker='o', facecolor='none', edgecolor='green', s=200)\n", + "\n", + "for point in closest_to_third_location:\n", + " plt.scatter(point[0], point[1], marker='o', facecolor='none', edgecolor='yellow', s=200)\n", + "\n", + "\n", + "\n", + "plt.xlabel('X-axis')\n", + "plt.ylabel('Y-axis')\n", + "plt.title('Input points (colored) and their closest neighbors (circled)')\n", + "plt.legend()\n", + "\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The two functions knn and nearest achieve the same objective. The only difference is that nearest adds input encryption and output decryption to simulate a client-server communication. The tests below show that their results are similar." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "assert (knn(first_location[0], first_location[1]) == nearest(first_location[0], first_location[1])).all()\n", + "assert (knn(second_location[0], second_location[1]) == nearest(second_location[0], second_location[1])).all()\n", + "assert (knn(third_location[0], third_location[1]) == nearest(third_location[0], third_location[1])).all()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "zama", + "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.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/NearestNeighbors/network.py b/NearestNeighbors/network.py new file mode 100644 index 0000000..3dbb51a --- /dev/null +++ b/NearestNeighbors/network.py @@ -0,0 +1,18 @@ +import geopandas as gpd +import config +import numpy + + +def get(): + all_restaurants = gpd.read_file(config.restaurants_filepath) + all_restaurants = all_restaurants.dropna(subset=["name", "cuisine"]) + sub_restaurants = all_restaurants.sample( + config.total_restaurants_number, random_state=42 + ) + point_coordinates = [ + list([int(point.coords[0][0]) % 10000, int(point.coords[0][1]) % 10000]) + for point in sub_restaurants["geometry"].to_crs("epsg:2154") + ] + point_coordinates = numpy.array(point_coordinates) + + return sub_restaurants, point_coordinates diff --git a/NearestNeighbors/requirements.txt b/NearestNeighbors/requirements.txt new file mode 100644 index 0000000..3c23eb5 --- /dev/null +++ b/NearestNeighbors/requirements.txt @@ -0,0 +1,334 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile requirements.in +# +altair==5.1.1 + # via streamlit +asttokens==2.4.0 + # via stack-data +attrs==23.1.0 + # via + # fiona + # jsonschema + # referencing +backcall==0.2.0 + # via ipython +blinker==1.6.2 + # via streamlit +branca==0.6.0 + # via + # folium + # streamlit-folium +cachetools==5.3.1 + # via streamlit +certifi==2023.7.22 + # via + # fiona + # pyproj + # requests +charset-normalizer==3.2.0 + # via requests +click==8.1.7 + # via + # click-plugins + # cligj + # fiona + # streamlit +click-plugins==1.1.1 + # via fiona +cligj==0.7.2 + # via fiona +cmake==3.27.5 + # via triton +comm==0.1.4 + # via ipykernel +concrete-python==2.0.0 + # via -r requirements.in +contourpy==1.1.1 + # via matplotlib +cycler==0.11.0 + # via matplotlib +debugpy==1.8.0 + # via ipykernel +decorator==5.1.1 + # via ipython +exceptiongroup==1.1.3 + # via ipython +executing==1.2.0 + # via stack-data +filelock==3.12.4 + # via + # torch + # triton +fiona==1.9.4.post1 + # via geopandas +folium==0.14.0 + # via + # -r requirements.in + # streamlit-folium +fonttools==4.42.1 + # via matplotlib +geopandas==0.14.0 + # via -r requirements.in +gitdb==4.0.10 + # via gitpython +gitpython==3.1.37 + # via streamlit +idna==3.4 + # via requests +importlib-metadata==6.8.0 + # via streamlit +ipykernel==6.25.2 + # via -r requirements.in +ipython==8.15.0 + # via ipykernel +jedi==0.19.0 + # via ipython +jinja2==3.1.2 + # via + # altair + # branca + # folium + # pydeck + # streamlit-folium + # torch +joblib==1.3.2 + # via scikit-learn +jsonschema==4.19.1 + # via altair +jsonschema-specifications==2023.7.1 + # via jsonschema +jupyter-client==8.3.1 + # via ipykernel +jupyter-core==5.3.1 + # via + # ipykernel + # jupyter-client +kiwisolver==1.4.5 + # via matplotlib +lit==16.0.6 + # via triton +mapclassify==2.6.0 + # via -r requirements.in +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.3 + # via jinja2 +matplotlib==3.8.0 + # via -r requirements.in +matplotlib-inline==0.1.6 + # via + # ipykernel + # ipython +mdurl==0.1.2 + # via markdown-it-py +mpmath==1.3.0 + # via sympy +nest-asyncio==1.5.8 + # via ipykernel +networkx==3.1 + # via + # concrete-python + # mapclassify + # torch +numpy==1.26.0 + # via + # -r requirements.in + # altair + # concrete-python + # contourpy + # folium + # mapclassify + # matplotlib + # pandas + # pyarrow + # pydeck + # scikit-learn + # scipy + # shapely + # streamlit +nvidia-cublas-cu11==11.10.3.66 + # via + # nvidia-cudnn-cu11 + # nvidia-cusolver-cu11 + # torch +nvidia-cuda-cupti-cu11==11.7.101 + # via torch +nvidia-cuda-nvrtc-cu11==11.7.99 + # via torch +nvidia-cuda-runtime-cu11==11.7.99 + # via torch +nvidia-cudnn-cu11==8.5.0.96 + # via torch +nvidia-cufft-cu11==10.9.0.58 + # via torch +nvidia-curand-cu11==10.2.10.91 + # via torch +nvidia-cusolver-cu11==11.4.0.1 + # via torch +nvidia-cusparse-cu11==11.7.4.91 + # via torch +nvidia-nccl-cu11==2.14.3 + # via torch +nvidia-nvtx-cu11==11.7.91 + # via torch +packaging==23.1 + # via + # altair + # geopandas + # ipykernel + # matplotlib + # streamlit +pandas==2.1.1 + # via + # altair + # geopandas + # mapclassify + # streamlit +parso==0.8.3 + # via jedi +pexpect==4.8.0 + # via ipython +pickleshare==0.7.5 + # via ipython +pillow==9.5.0 + # via + # matplotlib + # streamlit +platformdirs==3.10.0 + # via jupyter-core +prompt-toolkit==3.0.39 + # via ipython +protobuf==4.24.3 + # via streamlit +psutil==5.9.5 + # via ipykernel +ptyprocess==0.7.0 + # via pexpect +pure-eval==0.2.2 + # via stack-data +pyarrow==13.0.0 + # via streamlit +pydeck==0.8.1b0 + # via streamlit +pygments==2.16.1 + # via + # ipython + # rich +pyparsing==3.1.1 + # via matplotlib +pyproj==3.6.1 + # via geopandas +python-dateutil==2.8.2 + # via + # jupyter-client + # matplotlib + # pandas + # streamlit +pytz==2023.3.post1 + # via pandas +pyzmq==25.1.1 + # via + # ipykernel + # jupyter-client +referencing==0.30.2 + # via + # jsonschema + # jsonschema-specifications +requests==2.31.0 + # via + # folium + # streamlit +rich==13.5.3 + # via streamlit +rpds-py==0.10.3 + # via + # jsonschema + # referencing +scikit-learn==1.3.1 + # via mapclassify +scipy==1.11.2 + # via + # concrete-python + # mapclassify + # scikit-learn +shapely==2.0.1 + # via + # -r requirements.in + # geopandas +six==1.16.0 + # via + # asttokens + # fiona + # python-dateutil +smmap==5.0.1 + # via gitdb +stack-data==0.6.2 + # via ipython +streamlit==1.27.0 + # via + # -r requirements.in + # streamlit-folium +streamlit-folium==0.14.0 + # via -r requirements.in +sympy==1.12 + # via torch +tenacity==8.2.3 + # via streamlit +threadpoolctl==3.2.0 + # via scikit-learn +toml==0.10.2 + # via streamlit +toolz==0.12.0 + # via altair +torch==2.0.1 + # via + # concrete-python + # triton +tornado==6.3.3 + # via + # ipykernel + # jupyter-client + # streamlit +traitlets==5.10.0 + # via + # comm + # ipykernel + # ipython + # jupyter-client + # jupyter-core + # matplotlib-inline +triton==2.0.0 + # via torch +typing-extensions==4.8.0 + # via + # altair + # streamlit + # torch +tzdata==2023.3 + # via pandas +tzlocal==5.0.1 + # via streamlit +urllib3==2.0.5 + # via requests +validators==0.22.0 + # via streamlit +watchdog==3.0.0 + # via streamlit +wcwidth==0.2.6 + # via prompt-toolkit +wheel==0.41.2 + # via + # nvidia-cublas-cu11 + # nvidia-cuda-cupti-cu11 + # nvidia-cuda-runtime-cu11 + # nvidia-curand-cu11 + # nvidia-cusparse-cu11 + # nvidia-nvtx-cu11 +zipp==3.17.0 + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/NearestNeighbors/utils.py b/NearestNeighbors/utils.py new file mode 100644 index 0000000..5a0dc82 --- /dev/null +++ b/NearestNeighbors/utils.py @@ -0,0 +1,221 @@ +from concrete import fhe +from config import circuit_filepath, number_of_neighbors +from shapely.geometry import Point +import geopandas as gpd +import streamlit as st +import folium +from streamlit_folium import st_folium + + +def init_session(): + """Initialize the Streamlit session and layout configuration. + + Returns: + Streamlit.columns: A tuple of Streamlit columns for layout customization. + """ + st.set_page_config(layout="wide") + + if "markers" not in st.session_state: + st.session_state["markers"] = [] + if "server_side" not in st.session_state: + st.session_state["server_side"] = [] + if "client_side" not in st.session_state: + st.session_state["client_side"] = [] + + c1, c2, c3 = st.columns([1, 3, 1]) + + return c1, c2, c3 + + +def set_up_server(): + """Load a server instance from a specified circuit file + + Raises: + OSError: If there is an issue loading the FHE server. + + Returns: + concrete.fhe.compilation.server.Server: A server instance loaded from the circuit file. + """ + try: + server = fhe.Server.load(circuit_filepath) + except OSError as e: + raise OSError( + f"Something went wrong with the circuit. Make sure that the circuit \ + exists in {circuit_filepath}.If not run python generate_circuit.py." + ) from e + + return server + + +def set_up_client(serialized_client_specs): + """Generate a client instance from a specified circuit file + + Args: + serialized_client_specs (bytes): A serialized client specs + + Returns: + concrete.fhe.compilation.client.Client: A client instance created from the client specs + """ + + client_specs = fhe.ClientSpecs.deserialize(serialized_client_specs) + client = fhe.Client(client_specs) + + return client + + +def display_encrypted(encrypted_object): + """Display a truncated representation of an encrypted object as a hexadecimal string + + Args: + encrypted_object (bytes): A serialized encrypted object to display + + Returns: + str: A truncated hexadecimal representation of the encrypted object + """ + encoded_text = encrypted_object.hex() + res = "..." + encoded_text[-10:] + return res + + +def transform_point(longitude, latitude): + """Transform coordinates into an integer to be processed by the FHE circuit + + Args: + longitude (float): longitude of the point + latitude (float): latitude of the point + + Returns: + int, int: integers to be processed by the FHE circuit + """ + gdf = gpd.GeoDataFrame({"geometry": [Point(longitude, latitude)]}, crs="EPSG:4326") + gdf = gdf.to_crs("EPSG:2154") + x, y = gdf.geometry.iloc[0].x, gdf.geometry.iloc[0].y + x = int(x) % 10000 + y = int(y) % 10000 + + return x, y + + +def process_result(rest, result): + """Add the nearest restaurants in the map and in the client view + + Args: + rest (geopandas.DataFrame): list of restaurants + result (list[(int, int)]): list of the nearest neighbors returned by the algorithm + """ + add_to_client_side( + f"The {number_of_neighbors} closest restaurant to your location are:" + ) + for index, res in enumerate(result): + mask1 = ( + rest["geometry"] + .to_crs("epsg:2154") + .apply(lambda geom: int(geom.x) % 10000 == res[0]) + ) + mask2 = ( + rest["geometry"] + .to_crs("epsg:2154") + .apply(lambda geom: int(geom.y) % 10000 == res[1]) + ) + final_mask = mask1 & mask2 + result_df = rest[final_mask] + restaurant_info = f"{result_df.name.iloc[0]}, {result_df.cuisine.iloc[0]}" + add_marker(result_df.geometry, restaurant_info) + add_to_client_side(f"{index+1}. {restaurant_info}.") + + +def add_marker(coordinates, name): + """Add a marker with coordinates and a name to the Streamlit session. + + Args: + coordinates (Point): The coordinates of the marker + name (str): The name or label for the marker + """ + data = {"coordinates": coordinates, "name": name} + st.session_state["markers"].append(data) + + +def display_map(restaurants, returned_objects=None): + """Display the map with nodes and optional markers and paths. + + Args: + nodes (geopandas.DataFrame): A dataframe containing the nodes to display + returned_objects (List[str], optional): Objects to be returned when interacting with the map. Defaults to None. + + Returns: + Streamlit.FoliumMap: An interactive map displaying nodes and markers + """ + if "decrypted_result" in st.session_state: + m = restaurants.explore( + scheme="naturalbreaks", + tooltip="name", + popup=["name"], + name="Quadratic-Paris", + color="red", + marker_kwds=dict(radius=5, fill=True, name="node_id"), + ) + else: + m = restaurants.explore( + scheme="naturalbreaks", + tooltip="name", + popup=["name"], + name="Quadratic-Paris", + ) + + if "position" in st.session_state: + position = st.session_state["position"] + folium.Marker( + [position.y, position.x], popup="Starting point", tooltip="Starting point" + ).add_to(m) + + if "markers" in st.session_state: + for mrk in st.session_state["markers"]: + folium.Marker( + [mrk["coordinates"].y, mrk["coordinates"].x], + popup=mrk["name"], + tooltip=mrk["name"], + icon=folium.Icon(color="black", icon_color="#FFFF00"), + ).add_to(m) + + return st_folium(m, width=725, key="origin", returned_objects=returned_objects) + + +def add_to_server_side(message): + """Add a message to the server side of the view + + Args: + message (str): The message to be added to the server side + """ + st.session_state["server_side"].append(message) + + +def add_to_client_side(message): + """Add a message to the client side of the view + + Args: + message (str): The message to be added to the client side + """ + st.session_state["client_side"].append(message) + + +def display_server_side(): + """Display the messages stored in the server-side view.""" + st.write("**Server-side**") + for message in st.session_state["server_side"]: + st.write(message) + + +def display_client_side(): + """Display the messages stored in the client-side view.""" + st.write("**Client-side**") + for message in st.session_state["client_side"]: + st.write(message) + + +def restart_session(): + """Clear the session state to restart""" + if st.button("Restart"): + for key in st.session_state.items(): + if key[0] != "evaluation_key": + del st.session_state[key[0]] + st.rerun()