{"cells":[{"metadata":{},"cell_type":"markdown","source":"\n**I referred to these links**\n* https://developer.android.com/guide/topics/sensors/sensors_overview<br>\n* https://www.kaggle.com/iamleonie/intro-to-indoor-location-navigation<br>\n* https://github.com/danielmurray/adaptiv"},{"metadata":{"trusted":true},"cell_type":"code","source":"# a = \"5cd56b5ae2acfd2d33b58544/F4/5cf22b433f260a00082c8dea.txt\"\n# a = \"5cd56b5ae2acfd2d33b58544/F4/5cf22b453f260a00082c8dec.txt\"\n# a = \"5cd56b5ae2acfd2d33b58544/F4/5cf22b440395f600088f6501.txt\"\n# a = \"5cd56b5ae2acfd2d33b58544/F3/5cf22df20395f600088f653d.txt\"\n# a = \"5cd56b5ae2acfd2d33b58544/F3/5cf22df20395f600088f653d.txt\"\n\n# a = \"5cd56b77e2acfd2d33b5b22b/B1/5d09ed781a9498000952a05c.txt\"\n# a = \"5cd56b77e2acfd2d33b5b22b/B1/5d09edca1a9498000952a062.txt\"\n# a = \"5cd56b77e2acfd2d33b5b22b/F1/5d09f45324caf50008a4ef5b.txt\"\n# a = \"5cd56b77e2acfd2d33b5b22b/F4/5d09f97024caf50008a4eff1.txt\"\n# a = \"5cd56b77e2acfd2d33b5b22b/F2/5d09f5531a9498000952a11b.txt\"\n# a = \"5cd56b77e2acfd2d33b5b22b/F3/5d09f8181a9498000952a151.txt\"\na = \"5cd56b77e2acfd2d33b5b22b/F5/5d09fa831a9498000952a1a4.txt\"\n\n# a = \"5cd56865eb294480de7167b6/F2/5cfdd8fcffdc200008fddf30.txt\"\n# a = \"5cd56b6ae2acfd2d33b59c90/F1/5cfdc042ffdc200008fdde0c.txt\"\n# a = \"5cd56b70e2acfd2d33b5a44e/F3/5cf74718e36a480008125ed5.txt\"\n\n# a = \"5a0546857ecc773753327266/B1/5e1581d4f4c3420006d52109.txt\"\n# a = \"5cd969c839e2fc0b4afe7ff1/F5/5d0b3a544a58160008d75e26.txt\"\n\n# a = \"5cd56b77e2acfd2d33b5b310/B1/5cf79e5ac12efb0008551804.txt\"\n\nb = \"../input/indoor-location-navigation/train/\" + a","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"step_distance = 0.8\nw_height = 1.7\nm_trans = -5","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"import numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport math\n\nfrom dataclasses import dataclass\n\nimport matplotlib.pyplot as plt # visualization\nplt.rcParams.update({'font.size': 14})\nimport seaborn as sns # visualization\n\nimport warnings # Supress warnings \nwarnings.filterwarnings('ignore')\n\nfrom tqdm import tqdm\n\nimport json\nimport plotly.graph_objs as go\nfrom PIL import Image\n\nfrom datetime import datetime","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"# copy from https://github.com/location-competition/indoor-location-competition-20/blob/master/io_f.py\n\n@dataclass\nclass ReadData:\n    acce: np.ndarray\n    acce_uncali: np.ndarray\n    gyro: np.ndarray\n    gyro_uncali: np.ndarray\n    magn: np.ndarray\n    magn_uncali: np.ndarray\n    ahrs: np.ndarray\n    wifi: np.ndarray\n    ibeacon: np.ndarray\n    waypoint: np.ndarray\n\n\ndef read_data_file(data_filename):\n    acce = []\n    acce_uncali = []\n    gyro = []\n    gyro_uncali = []\n    magn = []\n    magn_uncali = []\n    ahrs = []\n    wifi = []\n    ibeacon = []\n    waypoint = []\n\n    with open(data_filename, 'r', encoding='utf-8') as file:\n        lines = file.readlines()\n\n    for line_data in lines:\n        line_data = line_data.strip()\n        if not line_data or line_data[0] == '#':\n            continue\n\n        line_data = line_data.split('\\t')\n\n        if line_data[1] == 'TYPE_WAYPOINT':\n            waypoint.append([int(line_data[0]), float(line_data[2]), float(line_data[3])])\n            continue\n       \n        if line_data[1] == 'TYPE_ACCELEROMETER':\n            acce.append([int(line_data[0]), float(line_data[2]), float(line_data[3]), float(line_data[4])])\n            continue\n        \n        if line_data[1] == 'TYPE_ACCELEROMETER_UNCALIBRATED':\n            acce_uncali.append([int(line_data[0]), float(line_data[2]), float(line_data[3]), float(line_data[4])])\n            continue\n        \n        if line_data[1] == 'TYPE_GYROSCOPE':\n            gyro.append([int(line_data[0]), float(line_data[2]), float(line_data[3]), float(line_data[4])])\n            continue\n\n        if line_data[1] == 'TYPE_GYROSCOPE_UNCALIBRATED':\n            gyro_uncali.append([int(line_data[0]), float(line_data[2]), float(line_data[3]), float(line_data[4])])\n            continue\n        \n        if line_data[1] == 'TYPE_MAGNETIC_FIELD':\n            magn.append([int(line_data[0]), float(line_data[2]), float(line_data[3]), float(line_data[4])])\n            continue\n\n        if line_data[1] == 'TYPE_MAGNETIC_FIELD_UNCALIBRATED':\n            magn_uncali.append([int(line_data[0]), float(line_data[2]), float(line_data[3]), float(line_data[4])])\n            continue\n\n        if line_data[1] == 'TYPE_ROTATION_VECTOR':\n            ahrs.append([int(line_data[0]), float(line_data[2]), float(line_data[3]), float(line_data[4])])\n            continue\n\n        if line_data[1] == 'TYPE_WIFI':\n            sys_ts = line_data[0]\n            ssid = line_data[2]\n            bssid = line_data[3]\n            rssi = line_data[4]\n            lastseen_ts = line_data[6]\n            wifi_data = [sys_ts, ssid, bssid, rssi, lastseen_ts]\n            wifi.append(wifi_data)\n            continue\n\n        if line_data[1] == 'TYPE_BEACON':\n            ts = line_data[0]\n            uuid = line_data[2]\n            major = line_data[3]\n            minor = line_data[4]\n            rssi = line_data[6]\n            ibeacon_data = [ts, '_'.join([uuid, major, minor]), rssi]\n            ibeacon.append(ibeacon_data)\n            continue\n        \n    \n    acce = np.array(acce)\n    acce_uncali = np.array(acce_uncali)\n    gyro = np.array(gyro)\n    gyro_uncali = np.array(gyro_uncali)\n    magn = np.array(magn)\n    magn_uncali = np.array(magn_uncali)\n    ahrs = np.array(ahrs)\n    wifi = np.array(wifi)\n    ibeacon = np.array(ibeacon)\n    waypoint = np.array(waypoint)\n    \n    return ReadData(acce, acce_uncali, gyro, gyro_uncali, magn, magn_uncali, ahrs, wifi, ibeacon, waypoint)\n\nsample_file = read_data_file(b)\n#     \"../input/indoor-location-navigation/train/5a0546857ecc773753327266/F2/5dccf516c04f060006e6e3c9.txt\")\n\nprint('acce shape:', sample_file.acce.shape)\nprint('acce_uncali shape:', sample_file.acce_uncali.shape)\nprint('gyro shape:', sample_file.gyro.shape)\nprint('gyro_uncali shape:', sample_file.gyro_uncali.shape)\nprint('magn shape:', sample_file.magn.shape)\nprint('magn_uncali shape:',sample_file.magn_uncali.shape)\nprint('ahrs shape:', sample_file.ahrs.shape)\nprint('wifi shape:', sample_file.wifi.shape)\nprint('ibeacon shape:', sample_file.ibeacon.shape)\nprint('waypoint shape:', sample_file.waypoint.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"\n# Detect steps by acceleration"},{"metadata":{"trusted":true},"cell_type":"code","source":"sample_file.acce","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"* Combine three accelerations\n* filtering"},{"metadata":{"trusted":true},"cell_type":"code","source":"mix_acce = np.sqrt(sample_file.acce[:,1:2]**2 + sample_file.acce[:,2:3]**2 + sample_file.acce[:,3:4]**2)\nmix_acce = np.concatenate([sample_file.acce[:,0:1], mix_acce], 1)\nmix_df = pd.DataFrame(mix_acce)\nmix_df.columns = [\"timestamp\",\"acce\"]\n# mix_df","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from scipy.signal import butter, lfilter\n\ndef butter_lowpass(cutoff, fs, order=5):\n    nyq = 0.5 * fs\n    normal_cutoff = cutoff / nyq\n    b, a = butter(order, normal_cutoff, btype='low', analog=False)\n    return b, a\n\ndef butter_lowpass_filter(data, cutoff, fs, order=5):\n    b, a = butter_lowpass(cutoff, fs, order=order)\n    y = lfilter(b, a, data)\n    return y\n\norder = 3\nfs = 50.0  # sample rate, Hz\n# fs = 100\n# cutoff = 3.667  # desired cutoff frequency of the filter, Hz\ncutoff = 3\n\nfiltered = butter_lowpass_filter(mix_df[\"acce\"], cutoff, fs, order)\nfiltered","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Actual linear movement distance（waypoint to waypoint）"},{"metadata":{"trusted":true},"cell_type":"code","source":"sample_file.waypoint","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"distancex = np.diff(sample_file.waypoint[:,1])\ndistancey = np.diff(sample_file.waypoint[:,2])\ndistance = np.sqrt(distancex**2 + distancey**2)\ndistance = np.insert(distance, 0, 0)\ndistance = np.concatenate([sample_file.waypoint[:,0:1], distance.reshape(-1, 1)], 1)\ndistance","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"dis_df = pd.DataFrame(distance)\ndis_df.columns = [\"timestamp\",\"distance\"]\ndis_df","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Time from waypoint to waypoint**"},{"metadata":{"trusted":true},"cell_type":"code","source":"q = 0\nd_time = []\n\nfor i in dis_df[\"timestamp\"]:\n    start_time = q\n    end_time = i\n    d_time.append(datetime.fromtimestamp(end_time/1000.0)-datetime.fromtimestamp(start_time/1000.0))\n    q = i\nd_time.pop(0)\n\nfor p in d_time:\n    print(p)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Filtered acceleration from waypoint to waypoint**"},{"metadata":{"trusted":true},"cell_type":"code","source":"fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(14, 5))\nplt.title(\"Filtered\")\nsns.lineplot(x=mix_df[\"timestamp\"], y=mix_df[\"acce\"], ax=ax, color='pink')\nsns.lineplot(x=mix_df[\"timestamp\"], y=filtered, ax=ax, color='cornflowerblue')\n\nfor i in dis_df[\"timestamp\"]:\n    ax.axvline(x=i, ymin=0, ymax=0.95,color='blue',ls='--',alpha=0.6)\n\nplt.tight_layout()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Detect steps**"},{"metadata":{"trusted":true},"cell_type":"code","source":"def peak_accel_threshold(data, timestamps, threshold):\n# def peak_accel_threshold(data, timestamps):\n    d_acc = []\n    last_state = 'below'\n    # below - less than threshold\n    # above - above the threshold\n    crest_troughs = 0\n    crossings = []\n\n    for i, datum in enumerate(data):\n        \n#         if len(d_acc) < 20:\n#             d_acc.append(datum)\n#             continue\n#         else:\n#             d_acc.append(datum)\n#             d_acc.pop(0)\n            \n#         threshold = (max(d_acc)+min(d_acc)/2*1)\n#         print(threshold)\n\n        current_state = last_state\n        if datum < threshold:\n            current_state = 'below'\n        elif datum > threshold:\n            current_state = 'above'\n\n        if current_state is not last_state:\n            if current_state is 'above':\n                crossing = [timestamps[i], threshold]\n                crossings.append(crossing)\n            else:\n                crossing = [timestamps[i], threshold]\n                crossings.append(crossing)\n\n            crest_troughs += 1\n        last_state = current_state\n\n    return np.array(crossings)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# threshold = 2.5\nthreshold = filtered.mean() * 1.1\n\ncrossings = peak_accel_threshold(filtered, mix_df[\"timestamp\"], threshold)\n# crossings = peak_accel_threshold(filtered, mix_df[\"timestamp\"])\n\nfig, ax = plt.subplots(nrows=1, ncols=1, figsize=(20, 5))\nplt.plot(mix_df[\"timestamp\"], filtered, 'b-', color='cornflowerblue')\nplt.plot(crossings.T[0], crossings.T[1], 'ro', linewidth=2)\nplt.xlabel('Time [sec]')\nfor i in dis_df[\"timestamp\"]:\n    ax.axvline(x=i, ymin=0, ymax=0.95,color='blue',ls='--',alpha=0.6)\nplt.grid()\nplt.legend()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"\nTwo red dots are walking"},{"metadata":{"trusted":true},"cell_type":"code","source":"step_sum = len(crossings)/2\ndistance = w_height * 0.4 * step_sum\nwp_distance_sum = dis_df[\"distance\"].sum()\nprint(step_sum,distance,wp_distance_sum)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Detect direction with magnetic field"},{"metadata":{"trusted":true},"cell_type":"code","source":"mag_df = pd.DataFrame(sample_file.magn)\nmag_df.columns = [\"timestamp\",\"x\",\"y\",\"z\"]\nmag_df","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"acce_df = pd.DataFrame(sample_file.acce)\nacce_df.columns = [\"timestamp\",\"ax\",\"ay\",\"az\"]\nacce_df","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"mag_df = pd.merge(mag_df,acce_df,on=\"timestamp\")\nmag_df","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"mag_df.dropna()\nmag_df","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**The direction the surveyor is facing at each time stamp**"},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"time_di_list = []\n\nfor i in mag_df.iterrows():\n    \n    gx,gy,gz = i[1][1],i[1][2],i[1][3]\n    ax,ay,az = i[1][4],i[1][5],i[1][6]\n    \n#     print(ax,ay,az)\n    \n    roll = math.atan2(ay,az)\n    pitch = math.atan2(-1*ax , (ay * math.sin(roll) + az * math.cos(roll)))\n    \n    q =  m_trans - math.degrees(math.atan2(\n        (gz*math.sin(roll)-gy*math.cos(roll)),(gx*math.cos(pitch) + gy*math.sin(roll)*math.sin(pitch) + gz*math.sin(pitch)*math.cos(roll))\n    )) -90\n    if q <= 0:\n        q += 360\n    time_di_list.append((i[1][0],q))\n\nd_list = [x[1] for x in time_di_list]\n# d_list","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(15, 5))\nplt.plot(mix_df[\"timestamp\"], filtered,'b-', color='cornflowerblue')\nplt.plot(crossings.T[0], crossings.T[1], 'ro', linewidth=2)\nplt.xlabel('Time [sec]')\nfor i in dis_df[\"timestamp\"]:\n    ax.axvline(x=i, ymin=0, ymax=0.95,color='blue',ls='--',alpha=0.6)\nax2 = ax.twinx()\nax2.set_ylabel(\"digree\")\nsns.lineplot(x=mix_df[\"timestamp\"], y=d_list[:len(mix_df[\"timestamp\"])], ax=ax2, color='black')\nplt.grid()\nplt.legend()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Direction when walking**"},{"metadata":{"trusted":true},"cell_type":"code","source":"steps = []\nstep_time = []\ndi_dict = dict(time_di_list)\n\nfor n,i in enumerate(crossings[:,:1]):\n    if n % 2 == 1:\n        continue\n    direct_now = di_dict[i[0]]\n    dx = math.sin(math.radians(direct_now))\n    dy = math.cos(math.radians(direct_now))\n    print(int(n/2+1),\"歩目/x:\",dx,\"/y:\",dy,\"/角度：\",direct_now)\n    steps.append((i[0],dx,dy))\n    step_time.append(i[0])\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Step-to-step time difference To the stride**"},{"metadata":{"trusted":true},"cell_type":"code","source":"step_dtime = np.diff(step_time)/1000\nstep_dtime = step_dtime.tolist()\nstep_dtime.insert(0,5)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Waypoint-to-waypoint moving distance step number * stride**"},{"metadata":{"trusted":true},"cell_type":"code","source":"wp_time = dis_df[\"timestamp\"].tolist()\nwp_time.pop(0)\n\nfirst_point = (sample_file.waypoint[0,1],sample_file.waypoint[0,2])\n(step_x, step_y) = first_point\npred_px = [step_x]\npred_py = [step_y]\n\nwp_idx = 0\nprint(\"WP:\",round(sample_file.waypoint[0,1],3),round(sample_file.waypoint[0,2],3),sample_file.waypoint[0,0])\nprint(\"------------------\")\nfor p,i in enumerate(steps):\n    step_distance = 0\n    if step_dtime[p] >= 1:\n        step_distance = w_height*0.25\n    elif step_dtime[p] >= 0.75:\n        step_distance = w_height*0.3\n    elif step_dtime[p] >= 0.5:\n        step_distance = w_height*0.4\n    elif step_dtime[p] >= 0.4:\n        step_distance = w_height*0.5\n    elif step_dtime[p] >= 0.3:\n        step_distance = w_height*0.6\n    else:\n        step_distance = w_height*0.4\n    \n    step_x += i[1]*step_distance\n    step_y += i[2]*step_distance\n    \n    if i[0] > wp_time[0]:\n        print(\"PD:\",round(step_x,3),round(step_y,3),i[0])\n        pred_px.append(step_x)\n        pred_py.append(step_y)\n        wp_time.pop(0)\n        wp_idx += 1\n        print(\"WP:\",round(sample_file.waypoint[wp_idx,1],3),round(sample_file.waypoint[wp_idx,2],3),sample_file.waypoint[wp_idx,0])\n        print(\"------------------\")\n    if not wp_time:\n        break\nif wp_time:\n    pred_px.append(step_x)\n    pred_py.append(step_y)\n    print(\"PD:\",round(step_x,3),round(step_y,3),i[0])\n    print(\"WP:\",round(sample_file.waypoint[wp_idx+1,1],3),round(sample_file.waypoint[wp_idx+1,2],3),sample_file.waypoint[wp_idx+1,0])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":false},"cell_type":"code","source":"def visualize_trajectory(trajectory, floor_plan_filename, width_meter, height_meter, title=None, mode='lines + markers + text', show=False):\n    \"\"\"\n    Copied from from https://github.com/location-competition/indoor-location-competition-20/blob/master/visualize_f.py\n\n    \"\"\"\n    fig = go.Figure()\n\n    # add trajectory\n    size_list = [6] * trajectory.shape[0]\n    size_list[0] = 10\n    size_list[-1] = 10\n\n    color_list = ['rgba(4, 174, 4, 0.5)'] * trajectory.shape[0]\n    color_list[0] = 'rgba(12, 5, 235, 1)'\n    color_list[-1] = 'rgba(235, 5, 5, 1)'\n\n    position_count = {}\n    text_list = []\n    for i in range(trajectory.shape[0]):\n        if str(trajectory[i]) in position_count:\n            position_count[str(trajectory[i])] += 1\n        else:\n            position_count[str(trajectory[i])] = 0\n        text_list.append('        ' * position_count[str(trajectory[i])] + f'{i}')\n    text_list[0] = 'Start 0'\n    text_list[-1] = f'End {trajectory.shape[0] - 1}'\n\n    fig.add_trace(go.Scatter(x=pred_px, y=pred_py, name=\"pred\"))\n    \n    fig.add_trace(\n        go.Scattergl(\n            x=trajectory[:, 0],\n            y=trajectory[:, 1],\n            mode=mode,\n            marker=dict(size=size_list, color=color_list),\n            line=dict(shape='linear', color='lightgrey', width=3, dash='dash'),\n            text=text_list,\n            textposition=\"top center\",\n            name='trajectory',\n        ))\n\n    # add floor plan\n    floor_plan = Image.open(floor_plan_filename)\n    fig.update_layout(images=[\n        go.layout.Image(\n            source=floor_plan,\n            xref=\"x\",\n            yref=\"y\",\n            x=0,\n            y=height_meter,\n            sizex=width_meter,\n            sizey=height_meter,\n            sizing=\"contain\",\n            opacity=1,\n            layer=\"below\",\n        )\n    ])\n\n    # configure\n    fig.update_xaxes(autorange=False, range=[0, width_meter])\n    fig.update_yaxes(autorange=False, range=[0, height_meter], scaleanchor=\"x\", scaleratio=1)\n    fig.update_layout(\n        title=go.layout.Title(\n            text=title or \"No title.\",\n            xref=\"paper\",\n            x=0,\n        ),\n        autosize=True,\n        width=800,\n        height=  800 * height_meter / width_meter,\n        template=\"plotly_white\",\n    )\n\n    if show:\n        fig.show()\n\n    return fig\n\ndef visualize_train_trajectory(path):\n    \"\"\"\n    Edited from \n    https://www.kaggle.com/ihelon/indoor-location-exploratory-data-analysis\n    \"\"\"\n    _id, floor = path.split(\"/\")[:2]\n    \n    train_floor_data = read_data_file(f\"../input/indoor-location-navigation/train/{path}\")\n    with open(f\"../input/indoor-location-navigation/metadata/{_id}/{floor}/floor_info.json\") as f:\n        train_floor_info = json.load(f)\n\n    return visualize_trajectory(\n        train_floor_data.waypoint[:, 1:3], \n        f\"../input/indoor-location-navigation/metadata/{_id}/{floor}/floor_image.png\",\n        train_floor_info[\"map_info\"][\"width\"], \n        train_floor_info[\"map_info\"][\"height\"],\n        f\"Visualization of {path}\"\n    )\nvisualize_train_trajectory(a)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4}