Synth_Tests.ipynb 19.4 KB
 Maximilian Schanner committed May 06, 2020 1 2 3 4 5 6 7 8 9 10 11 12 { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Synthetic tests\n", "The purpose of this notebook is to show some synthetic tests for the CORBASS algorithm. Synthetic data are generated using the notebook Gen_Data.ipynb. First some imports:" ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 13  "execution_count": null,  Maximilian Schanner committed May 06, 2020 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81  "metadata": {}, "outputs": [], "source": [ "# Imports\n", "import sys\n", "import os\n", "# relative import\n", "sys.path.append(os.path.abspath('') + '/../')\n", "\n", "import numpy as np\n", "import pandas as pd\n", "\n", "from matplotlib import pyplot as plt, colors, cm\n", "from cartopy import crs as ccrs\n", "\n", "from scipy.stats import gaussian_kde\n", "\n", "import pyfield\n", "from corbass.inversion import Inversion\n", "from dip_lin_inversion import Dip_Lin_Inversion\n", "\n", "glob_proj = ccrs.Mollweide(central_longitude=0)\n", "# a handy plotting function\n", "def plot_field(lat, lon, field, names=None, proj=glob_proj, cbar=True, cmap='RdBu',\n", " vmin=None, vmax=None, symm=False, cbarlabel=r'$\\mu$T'):\n", " fig, ax = plt.subplots(1, 3, figsize=(17, 10), subplot_kw={'projection': proj})\n", " bnds = ax[0].get_position().bounds\n", " scl = bnds[3]\n", " spc = 0.2*scl\n", " cbar_hght = 0.07*scl\n", " if cbar and cbarlabel:\n", " fig.text(bnds[0]-0.1*spc, bnds[1]+spc-0.5*cbar_hght, cbarlabel,\n", " va='center', ha='right')\n", " for it in range(3):\n", " bnds = ax[it].get_position().bounds\n", " ax[it].tripcolor(lat, lon, field[it::3], cmap=cmap)\n", " ax[it].coastlines(zorder=4)\n", " if names:\n", " ax[it].set_title('NEZ'[it])\n", " if cbar:\n", " if vmin is not None:\n", " _vmin = vmin\n", " else:\n", " _vmin = min(field[it::3])\n", " if vmax is not None:\n", " _vmax = vmax\n", " else:\n", " _vmax = max(field[it::3])\n", " \n", " if symm:\n", " _vmax = max(abs(_vmax), abs(_vmin))\n", " _vmin = -_vmax\n", "\n", " colax = fig.add_axes([bnds[0], \n", " bnds[1]+spc-cbar_hght, \n", " bnds[2], \n", " cbar_hght])\n", " norm = colors.Normalize(vmin=_vmin,\n", " vmax=_vmax)\n", " cbar = fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), cax=colax,\n", " orientation='horizontal')\n", " return fig, ax" ] }, { "cell_type": "markdown", "metadata": {}, "source": [  Stefan Mauerberger committed May 07, 2020 82  "We start by setting some basic variables we use throughout the notebook. Similar to Gen_Data.ipynb, we use the IGRF-13 model as a reference. "  Maximilian Schanner committed May 06, 2020 83 84 85 86  ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 87  "execution_count": null,  Maximilian Schanner committed May 06, 2020 88 89 90 91  "metadata": {}, "outputs": [], "source": [ "# the reference coefficients from IGRF\n",  Stefan Mauerberger committed May 07, 2020 92  "IGRF = pd.read_csv('https://www.ngdc.noaa.gov/IAGA/vmod/coeffs/igrf13coeffs.txt', header=0, delim_whitespace=True, skiprows=3)\n",  Maximilian Schanner committed May 06, 2020 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144  "ref_coeffs = IGRF[['2020.0']].to_numpy().flatten()\n", "# retrieve the maximal degree using pyfield and the index of the last entry in ref_coeffs\n", "l_max = pyfield.i2lm_l(len(ref_coeffs)-1)\n", "\n", "# the approximate number of design points\n", "n_points = 2000\n", "# parameters for the inversions\n", "r_ref = 2800\n", "lamb = 16000\n", "epsilon = 1.34\n", "rho = 5000\n", "# the axial dipole to linearize around in nT\n", "lin_dip = -23e3\n", "\n", "# various data files, generated using the notebook Gen_Data.ipynb\n", "# data without noise, at random locations, no records missing\n", "data_clean_complete = pd.read_csv('../dat/synth_data_clean_complete.csv', skiprows=2)\n", "# data without noise, at random locations, 80% are incomplete (i.e. at least one component is missing)\n", "data_clean_incomplete = pd.read_csv('../dat/synth_data_clean_incomplete.csv', skiprows=2)\n", "# same as above, but at locations taken from GEOMAGIA\n", "data_clean_incomplete_real = pd.read_csv('../dat/synth_data_clean_incomplete_real.csv', skiprows=2)\n", "# data with artificial noise, at locations taken from GEOMAGIA, no records missing\n", "data_noisy_complete = pd.read_csv('../dat/synth_data_noisy_complete.csv', skiprows=2)\n", "# same as above, but records that are missing in GEOMAGIA have been excluded\n", "data_noisy_incomplete = pd.read_csv('../dat/synth_data_noisy_incomplete.csv', skiprows=2)\n", "# same as above, but the noise level has been significantly increased\n", "data_very_noisy_incomplete = pd.read_csv('../dat/synth_data_very_noisy_incomplete.csv', skiprows=2)\n", "\n", "data_labels = ['Clean Complete', \n", " 'Clean Incomplete',\n", " 'Clean Incomplete Real', \n", " 'Noisy Complete', \n", " 'Noisy Incomplete', \n", " 'Very Noisy Incomplete']\n", "data_lst = [data_clean_complete, \n", " data_clean_incomplete,\n", " data_clean_incomplete_real,\n", " data_noisy_complete, \n", " data_noisy_incomplete, \n", " data_very_noisy_incomplete]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Preliminaries\n", "We perform a detailed test for one dataset. The other datasets are compared in a table at the end of this section." ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 145  "execution_count": null,  Maximilian Schanner committed May 06, 2020 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160  "metadata": {}, "outputs": [], "source": [ "data_detail = data_clean_complete" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We start by generating design points and constructing the reference field at this design points." ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 161  "execution_count": null,  Maximilian Schanner committed May 06, 2020 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197  "metadata": {}, "outputs": [], "source": [ "# get design points using CORBASS routines\n", "x_desi, n_act = Inversion.desi_points(None, n_points)\n", "# latitude and longitude of reference points for plotting\n", "lat, lon, _ = glob_proj.transform_points(ccrs.Geodetic(),\n", " x_desi[1],\n", " 90-x_desi[0]).T\n", "\n", "# construct a basis using pyfield\n", "dspharm = np.empty((len(ref_coeffs), 3*n_act), order='F')\n", "pyfield.dspharm(src=pyfield.SOURCE_INTERNAL,\n", " gSys=pyfield.SYS_GEO,\n", " atSys=pyfield.SYS_GEO,\n", " atForm=pyfield.COOR_CLR,\n", " bSys=pyfield.SYS_GEO,\n", " bForm=pyfield.FIELD_NED,\n", " lmax=l_max,\n", " R=pyfield.REARTH,\n", " t=0.,\n", " at=x_desi[:3, :],\n", " B=dspharm)\n", "# reference field is the scalar product of coefficients and basis\n", "ref_field = np.dot(ref_coeffs, dspharm)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's have a look at the **reference field** by using the handy plotting routine defined above. On top of the north component we also plot the record locations in pink." ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 198  "execution_count": null,  Maximilian Schanner committed May 06, 2020 199  "metadata": {},  Stefan Mauerberger committed May 07, 2020 200  "outputs": [],  Maximilian Schanner committed May 06, 2020 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217  "source": [ "fig, ax = plot_field(lat, lon, ref_field/1000, names='NEZ', symm=True);\n", "ax[0].scatter(data_detail[['lon']], data_detail[['lat']],\n", " s=12, marker='o', lw=0., transform=ccrs.Geodetic(),\n", " c='C6', zorder=5);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Detailed Test\n", "Now we can get to the actual tests. First setup the inversion classes:" ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 218  "execution_count": null,  Maximilian Schanner committed May 06, 2020 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236  "metadata": {}, "outputs": [], "source": [ "# CORBASS strategy\n", "ours = Inversion(data_detail)\n", "# linearization about a given axial dipole\n", "dipl = Dip_Lin_Inversion(data_detail)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can run the inversions:" ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 237  "execution_count": null,  Maximilian Schanner committed May 06, 2020 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255  "metadata": { "scrolled": false }, "outputs": [], "source": [ "ours_mean, ours_cov = ours.field_inversion(r_ref, lamb, epsilon, rho, n_points)\n", "dipl_mean, dipl_cov = dipl.field_inversion(r_ref, lamb, epsilon, rho, lin_dip, n_points)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can again look at the resulting fields using the plotting routine. We also show the difference in the third row." ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 256  "execution_count": null,  Maximilian Schanner committed May 06, 2020 257  "metadata": {},  Stefan Mauerberger committed May 07, 2020 258  "outputs": [],  Maximilian Schanner committed May 06, 2020 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274  "source": [ "# rescale to uT to have smaller numbers in the legend\n", "plot_field(lat, lon, ours_mean/1000, names='NEZ', symm=True);\n", "plot_field(lat, lon, dipl_mean/1000, symm=True);\n", "plot_field(lat, lon, np.abs(ours_mean-ref_field)/1000, vmin=0, cmap='binary');" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It is further interesting to have a look at the posterior standard deviations, the last row again shows the difference:" ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 275  "execution_count": null,  Maximilian Schanner committed May 06, 2020 276  "metadata": {},  Stefan Mauerberger committed May 07, 2020 277  "outputs": [],  Maximilian Schanner committed May 06, 2020 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305  "source": [ "ours_sd = np.sqrt(np.diag(ours_cov))\n", "dipl_sd = np.sqrt(np.diag(dipl_cov))\n", "# rescale to uT to have smaller numbers at the legend\n", "_, ours_ax = plot_field(lat, lon, ours_sd/1000, names='NEZ', vmin=0, cmap='binary')\n", "_, dipl_ax = plot_field(lat, lon, dipl_sd/1000, vmin=0, cmap='binary')\n", "plot_field(lat, lon, (ours_sd-dipl_sd)/1000, symm=True);\n", "\n", "# switch for plotting the data locations on top of the standard deviation\n", "if False:\n", " for it in range(3):\n", " ours_ax[it].scatter(data_detail[['lon']], data_detail[['lat']],\n", " s=12, marker='o', lw=0, transform=ccrs.Geodetic(),\n", " c='C1', zorder=5);\n", " dipl_ax[it].scatter(data_detail[['lon']], data_detail[['lat']],\n", " s=12, marker='o', lw=0, transform=ccrs.Geodetic(),\n", " c='C1', zorder=5);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To allow better comparison we calculate the absolute error w.r.t. the reference field." ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 306  "execution_count": null,  Maximilian Schanner committed May 06, 2020 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322  "metadata": {}, "outputs": [], "source": [ "ours_ae = np.abs(ours_mean-ref_field)\n", "dipl_ae = np.abs(dipl_mean-ref_field)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We analyze the distribution of the absolute error for the two models and fit a distribution to the histogram using Gaussian kernel density estimation from scipy, to access the mode." ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 323  "execution_count": null,  Maximilian Schanner committed May 06, 2020 324  "metadata": {},  Stefan Mauerberger committed May 07, 2020 325  "outputs": [],  Maximilian Schanner committed May 06, 2020 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368  "source": [ "n_bins = 40\n", "\n", "fig, ax = plt.subplots(1, 3, figsize=(17, 10))\n", "\n", "ax[0].set_title('ours');\n", "_, ours_bins, _ = ax[0].hist(ours_ae, bins=n_bins, density=True);\n", "\n", "ours_arr = np.linspace(ours_bins.min(), ours_bins.max(), 2*n_bins+1)\n", "ours_smooth = gaussian_kde(ours_ae)(ours_arr)\n", "ours_mode = ours_arr[np.argmax(ours_smooth)]\n", "\n", "ax[0].plot(ours_arr, ours_smooth, ls='--', color='black', lw=2)\n", "ax[0].set_xlabel('absolute error [nT]')\n", "ax[0].set_yticks([]);\n", "\n", "ax[1].set_title('dipl');\n", "_, dipl_bins, _ = ax[1].hist(dipl_ae, bins=n_bins, density=True, color='C1');\n", "\n", "dipl_arr = np.linspace(dipl_bins.min(), dipl_bins.max(), 2*n_bins+1)\n", "dipl_smooth = gaussian_kde(dipl_ae)(dipl_arr)\n", "dipl_mode = dipl_arr[np.argmax(dipl_smooth)]\n", "\n", "ax[1].plot(dipl_arr, dipl_smooth, ls='--', color='black', lw=2)\n", "ax[1].set_xlabel('absolute error [nT]')\n", "ax[1].set_yticks([]);\n", "\n", "ax[2].set_title('combined');\n", "ax[2].hist(ours_ae, bins=n_bins, density=True, histtype='step');\n", "ax[2].hist(dipl_ae, bins=n_bins, density=True, histtype='step');\n", "ax[2].set_xlabel('absolute error [nT]');\n", "ax[2].set_yticks([]);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As a summary we calculate the mean absolute error (MAE), the mode and the 16- and 84-percentiles of the distribution." ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 369  "execution_count": null,  Maximilian Schanner committed May 06, 2020 370  "metadata": {},  Stefan Mauerberger committed May 07, 2020 371  "outputs": [],  Maximilian Schanner committed May 06, 2020 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394  "source": [ "ours_16, ours_84 = np.quantile(ours_ae, (0.16, 0.84))\n", "dipl_16, dipl_84 = np.quantile(dipl_ae, (0.16, 0.84))\n", "\n", "print(f\"\\tMAE\\t\\tmode\\t\\t16-percentile\\t84-percentile\")\n", "print(f\"ours:\\t{np.sum(ours_ae)/n_points:.2f} nT\"\n", " f\"\\t{ours_mode:.2f} nT\\t{np.min(ours_ae)+ours_16:.2f} nT\\t{np.min(ours_ae)+ours_84:.2f} nT\")\n", "print(f\"dipl:\\t{np.sum(dipl_ae)/n_points:.2f} nT\"\n", " f\"\\t{dipl_mode:.2f} nT\\t{np.min(dipl_ae)+dipl_16:.2f} nT\\t{np.min(dipl_ae)+dipl_84:.2f} nT\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For the default choice of data (clean_complete), both models are able to reproduce the reference field and it's hard to tell by visual inspection of the models which one performs better. The means deviate up to 40%, while the standard deviations look very similar for both models. A more objective comparison is offered by the statistics of the absolute error. Here it is revealed, that the CORBASS strategy performs slightly better on this dataset. \n", "\n", "## Comparison for multiple datasets\n", "We now produce a table reporting the relevant quantities for all datasets." ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 395  "execution_count": null,  Maximilian Schanner committed May 06, 2020 396  "metadata": {},  Stefan Mauerberger committed May 07, 2020 397  "outputs": [],  Maximilian Schanner committed May 06, 2020 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450  "source": [ "def abs_error_stats(field, n_bins=n_bins):\n", " \"\"\" Convenience funtion reporting the relevant quantities for the absolute error statistics \"\"\"\n", " # CORBASS strategy\n", " ae = np.abs(field-ref_field)\n", " mean = np.sum(ae)/len(ae) \n", " q_16, q_84 = np.quantile(ae, (0.16, 0.84))\n", "\n", " _, bins = np.histogram(ae, bins=n_bins, density=True);\n", " arr = np.linspace(bins.min(), bins.max(), 2*n_bins+1)\n", " smooth = gaussian_kde(ae)(arr)\n", " mode = arr[np.argmax(smooth)]\n", " \n", " return mean, mode, q_16, q_84, np.min(ae)\n", "\n", "\n", "print(f\"\\tMAE\\t\\tmode\\t\\t16-percentile\\t84-percentile\")\n", "print(f\"\\t-------------------------------------------------------------\")\n", "for name, data in zip(data_labels, data_lst):\n", " print(name)\n", " ours = Inversion(data)\n", " ours_field, _ = ours.field_inversion(r_ref, lamb, epsilon, rho, n_points)\n", " ours_stats = abs_error_stats(ours_field)\n", " print(f\"ours:\\t{ours_stats[0]:.2f} nT\\t\"\n", " f\"{ours_stats[1]:.2f} nT\\t\"\n", " f\"{ours_stats[4]+ours_stats[2]:.2f} nT\\t\"\n", " f\"{ours_stats[4]+ours_stats[3]:.2f} nT\")\n", "\n", " dipl = Dip_Lin_Inversion(data)\n", " dipl_field, _ = dipl.field_inversion(r_ref, lamb, epsilon, rho, lin_dip, n_points)\n", " dipl_stats = abs_error_stats(dipl_field)\n", " print(f\"dipl:\\t{dipl_stats[0]:.2f} nT\\t\"\n", " f\"{dipl_stats[1]:.2f} nT\\t\"\n", " f\"{dipl_stats[4]+dipl_stats[2]:.2f} nT\\t\"\n", " f\"{dipl_stats[4]+dipl_stats[3]:.2f} nT\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Except for the clean_incomplete data, the proposed strategy performs slightly better than the linearization about a fixed dipole." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Appendix" ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 451  "execution_count": null,  Maximilian Schanner committed May 06, 2020 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467  "metadata": {}, "outputs": [], "source": [ "# Computing the appendix is computationally demanding, so we implement a switch\n", "appendix = True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As it is interesting to see the influence of the strength of the dipole used for linearization, we produce a plot of the used $g_1^0$ vs. the mean of the squared deviations." ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 468  "execution_count": null,  Maximilian Schanner committed May 06, 2020 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485  "metadata": {}, "outputs": [], "source": [ "if appendix:\n", " dipl = Dip_Lin_Inversion(data_detail)\n", " dip_arr = np.linspace(-70e3, 30e3, 51)\n", " # 0 is not a valid linearization point\n", " dip_arr = np.delete(dip_arr, np.argwhere(dip_arr == 0))\n", "\n", " mae = np.empty_like(dip_arr)\n", " for it in range(len(dip_arr)):\n", " mean, _ = dipl.field_inversion(r_ref, lamb, epsilon, rho, dip_arr[it], n_points)\n", " mae[it] = np.sum(np.abs(mean-ref_field))/n_points" ] }, { "cell_type": "code",  Stefan Mauerberger committed May 07, 2020 486  "execution_count": null,  Maximilian Schanner committed May 06, 2020 487  "metadata": {},  Stefan Mauerberger committed May 07, 2020 488  "outputs": [],  Maximilian Schanner committed May 06, 2020 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535  "source": [ "if appendix: \n", " from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes, mark_inset\n", "\n", " ours_mae = np.sum(ours_ae)/n_points\n", " # the index at which the dipole flips its sign\n", " flip = np.argmax(dip_arr[np.argwhere(dip_arr < 0)])+1\n", "\n", " fig, ax = plt.subplots(1, 1, figsize=(10, 10)) \n", " ax.plot(dip_arr[:flip]/1000., mae[:flip], color='C0', label='Linearized dipole')\n", " ax.plot(dip_arr[flip:]/1000., mae[flip:], color='C0')\n", " ax.set_xlabel(r'$g_1^0$ [$\\mu$T]')\n", " ax.set_ylabel(r'MAE [nT]')\n", " ax.axvline(ref_coeffs[0]/1000, label='True dipole', color='black', ls='--')\n", " ax.axhline(ours_mae, color='C1', ls='--', label='Ours')\n", "\n", "\n", " axins = zoomed_inset_axes(ax, 4.5, loc='upper left')\n", " inds = np.argwhere(np.abs((dip_arr-ref_coeffs[0])/ref_coeffs[0]) < 0.25)\n", " axins.plot(dip_arr[:flip]/1000., mae[:flip], marker='o', color='C0')\n", " axins.axvline(ref_coeffs[0]/1000, color='black', ls='--')\n", " axins.axhline(ours_mae, color='C1', ls='--')\n", " axins.set_xlim((1.25*ref_coeffs[0]/1000, 0.75*ref_coeffs[0]/1000))\n", " axins.set_xticks([])\n", " axins.set_ylim((0.5*ours_mae, 1.5*ours_mae))\n", " axins.set_yticks([])\n", " mark_inset(ax, axins, loc1=3, loc2=4, fc=\"none\", ec=\"0.5\")\n", " ax.legend(loc='center right');" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3",  Stefan Mauerberger committed May 07, 2020 536  "version": "3.6.9"  Maximilian Schanner committed May 06, 2020 537 538 539 540 541  } }, "nbformat": 4, "nbformat_minor": 2 }