{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# VOBS (Virtual Observatories)\n", "\n", "> Abstract: Geomagnetic Virtual Observatories (GVOs) spatially aggregate magnetic field measurements made by spacecraft at different times, in order to create time series of the magnetic field at 300 fixed locations (the virtual observatories). Data collected near each virtual observatory are combined by fitting to a local potential field in order to produce estimates of the observed field at monthly and four-monthly cadences. The resulting data products are suitable for studying Earth's core dynamo, being comparable to classical ground observatories but with uniform global coverage. For more information, see the [project webpage](https://www.space.dtu.dk/english/research/projects/project-descriptions/geomagnetic-virtual-observatories) and [Hammer, M.D., et al. Geomagnetic Virtual Observatories: monitoring geomagnetic secular variation with the Swarm satellites. Earth Planets Space 73, 54 (2021). https://doi.org/10.1186/s40623-021-01357-9](https://doi.org/10.1186/s40623-021-01357-9)\n", "\n", "See also:\n", "\n", " - https://nbviewer.jupyter.org/github/pacesm/jupyter_notebooks/blob/master/VOBS/VOBS_data_access.ipynb\n", " - https://nbviewer.jupyter.org/github/pacesm/jupyter_notebooks/blob/master/VOBS/VOBS_Swarm_CHAMP_Cryosat2.ipynb\n", " - http://www.spacecenter.dk/files/magnetic-models/GVO/GVO_Product_Definition.pdf\n", " - (link to dashboard TBD)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Show important version numbers to help debugging\n", "%load_ext watermark\n", "%watermark -i -v -p viresclient,pandas,xarray,matplotlib,numpy" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import datetime as dt\n", "import numpy as np\n", "import xarray as xr\n", "import matplotlib as mpl\n", "import matplotlib.pyplot as plt\n", "import cartopy.crs as ccrs\n", "import cartopy.feature as cfeature\n", "from tqdm import tqdm\n", "from viresclient import SwarmRequest" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Quickstart\n", "\n", "Core field predictions (`\"B_CF\"` and associated uncertainty `\"sigma_CF\"`) from the Swarm 1-monthly VOBS product, `\"SW_OPER_VOBS_1M_2_\"`, can be fetched with:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from viresclient import SwarmRequest\n", "import datetime as dt\n", "\n", "request = SwarmRequest()\n", "request.set_collection(\"SW_OPER_VOBS_1M_2_\")\n", "request.set_products(\n", " measurements=[\"SiteCode\", \"B_CF\", \"sigma_CF\"]\n", ")\n", "data = request.get_between(\n", " dt.datetime(2000, 1, 1),\n", " dt.datetime.now(),\n", " asynchronous=False, # Process synchronously (suitable for small data)\n", " show_progress=False # Disable the downloading progress bar\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can load the data as a pandas dataframe (use `expand=True` to split the vectors out into columns for each component):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df = data.as_dataframe(expand=True)\n", "df.tail()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Or as an xarray dataset (use `reshape=True` to get the data in a higher-dimensional form that more naturally accommodates the shape of the data):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ds = data.as_xarray(reshape=True)\n", "ds" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `\"SiteCode\"` variable is set as an index of the dataset (only when we specify `reshape=True` above) so we can quickly extract the data for a particular virtual observatory:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ds.sel(SiteCode=\"N90E000\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This enables rapid visualisation of the data from just that observatory, for example with:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ds.sel(SiteCode=\"N90E000\").sel(NEC=\"N\")[\"B_CF\"].plot.line(x=\"Timestamp\");" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ds.sel(SiteCode=\"N90E000\")[\"B_CF\"].plot.line(x=\"Timestamp\", col=\"NEC\", sharey=False);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A quick graph of the virtual observatory locations, and how we can extract the list of their names in the `\"SiteCode\"` variable:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ds.isel(Timestamp=0).plot.scatter(y=\"Latitude\", x=\"Longitude\");" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ds[\"SiteCode\"].values[0:10]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Product collection details\n", "\n", "There are five products available, available through VirES under collections of the same name:\n", "\n", "| Collection Name | Description |\n", "|---|---|\n", "| `SW_OPER_VOBS_1M_2_` | Swarm 1 month data from all virtual observatories |\n", "| `SW_OPER_VOBS_4M_2_` | Swarm 4 month data from all virtual observatories |\n", "| `CH_OPER_VOBS_4M_2_` | CHAMP 4 month data from all virtual observatories |\n", "| `CR_OPER_VOBS_4M_2_` | Cryosat-2 4 month data from all virtual observatories |\n", "| `OR_OPER_VOBS_4M_2_` | Ørsted 4 month data from all virtual observatories |\n", "| `CO_OPER_VOBS_4M_2_` | Composite (Ørsted, CHAMP, Cryosat-2, Swarm) 4 month data from all virtual observatories |\n", "\n", "These collections each contain the variables:\n", "\n", "| Variable | Unit | Dimension | Description |\n", "|---|---|---|---|\n", "| `SiteCode` | $$-$$ | char [7] | virtual observatory identifier |\n", "| `Timestamp` | $$-$$ | scalar | UTC time of observation |\n", "| `Latitude` | $$\\text{deg}$$ | scalar | ITRF geocentric latitude |\n", "| `Longitude` | $$\\text{deg}$$ | scalar | ITRF geocentric longitude |\n", "| `Radius` | $$\\text{m}$$ | scalar | ITRF geocentric radius |\n", "| `B_CF` | $$\\text{nT}$$ | vector [3] | Core magnetic field vector in ITRF NEC frame. |\n", "| `B_OB` | $$\\text{nT}$$ | vector [3] | Observed magnetic field vector in ITRF NEC frame. |\n", "| `sigma_CF` | $$\\text{nT}$$ | vector [3] | Estimated error of the core magnetic field vector in ITRF NEC frame. |\n", "| `sigma_OB` | $$\\text{nT}$$ | vector [3] | Estimated error of the observed magnetic field vector in ITRF NEC frame. |\n", "\n", "The secular variation estimates are available within separate collections (because of the different time sampling points) with `:SecularVariation` appended:\n", "\n", "| Collection Name | Description |\n", "|---|---|\n", "| `SW_OPER_VOBS_1M_2_:SecularVariation` | Swarm 1 month secular variation data from all virtual observatories |\n", "| `SW_OPER_VOBS_4M_2_:SecularVariation` | Swarm 4 month secular variation data from all virtual observatories |\n", "| `CH_OPER_VOBS_4M_2_:SecularVariation` | CHAMP 4 month secular variation data from all virtual observatories |\n", "| `CR_OPER_VOBS_4M_2_:SecularVariation` | Cryosat-2 4 month secular variation data from all virtual observatories |\n", "| `OR_OPER_VOBS_4M_2_:SecularVariation` | Ørsted 4 month secular variation data from all virtual observatories |\n", "| `CO_OPER_VOBS_4M_2_:SecularVariation` | Composite (Ørsted, CHAMP, Cryosat-2, Swarm) 4 month secular variation data from all virtual observatories |\n", "\n", "These collections similarly contain the variables:\n", "\n", "| Variable | Unit | Dimension | Description | \n", "|---|---|---|---|\n", "| `SiteCode` | $$-$$ | char [7] | virtual observatory identifier |\n", "| `Timestamp` | $$-$$ | scalar | UTC time of observation |\n", "| `Latitude` | $$\\text{deg}$$ | scalar | ITRF geocentric latitude |\n", "| `Longitude` | $$\\text{deg}$$ | scalar | ITRF geocentric longitude |\n", "| `Radius` | $$\\text{m}$$ | scalar | ITRF geocentric radius |\n", "| `B_SV` | $$\\text{nT}/\\text{yr}$$ | vector [3] | Field secular variation vector in ITRF NEC frame. |\n", "| `sigma_SV` | $$\\text{nT}/\\text{yr}$$ | vector [3] | Estimated error of the field secular variation vector in ITRF NEC frame. |\n", "\n", "Sub-collections are also defined for each virtual observatory, named according to `SiteCode`, so that data from a specific observatory can be fetched alone by specifying collections named like `\"SW_OPER_VOBS_1M_2_:N65W051\"` or `\"SW_OPER_VOBS_1M_2_:SecularVariation:N65W051\"`.\n", "\n", "NB: VirES provides the data in the NEC frame in order to be consistent with the other Swarm products available through VirES. This is in contrast to the source files which are provided in the (Radial, Theta, Phi) frame.\n", "\n", "---\n", "\n", "The list of available observatory names (i.e. `SiteCode`) can be queried with:\n", "\n", "```python\n", "request = SwarmRequest()\n", "request.available_observatories(\"SW_OPER_VOBS_1M_2_\")\n", "```\n", "\n", "---\n", "\n", "Magnetic model predictions can also be fetched directly just as with the `MAGx_LR` products (but it is currently *not* possible to directly fetch the secular variation predictions of models).\n", "\n", "For example, we can fetch the data for a specific observatory, together with IGRF predictions:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "request = SwarmRequest()\n", "request.set_collection(\"SW_OPER_VOBS_1M_2_:N65W051\")\n", "request.set_products(\n", " measurements=[\"SiteCode\", \"B_OB\", \"sigma_OB\", \"B_CF\", \"sigma_CF\"],\n", " models=[\"IGRF\"]\n", ")\n", "data = request.get_between(\n", " dt.datetime(2016, 1, 1),\n", " dt.datetime(2017, 1, 1),\n", " asynchronous=False,\n", " show_progress=False\n", ")\n", "ds = data.as_xarray()\n", "ds" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A quick plot comparing the virtual observatory core field estimates with the IGRF predictions:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ds.plot.scatter(x=\"Timestamp\", y=\"B_CF\", col=\"NEC\", sharey=False, figsize=(15,3))\n", "ds.plot.scatter(x=\"Timestamp\", y=\"B_NEC_IGRF\", col=\"NEC\", sharey=False, figsize=(15,3));" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Fetching and merging all available data\n", "\n", "Here is an example of loading data from all the products. We merge secular variation (SV) into datasets containing also the field measurements by defining a `Timestamp_SV` variable, rotate from the NEC frame to the RTP (Radial, Theta, Phi) frame, and collect the five products into a dictionary of five items." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import datetime as dt\n", "import numpy as np\n", "import xarray as xr\n", "import matplotlib as mpl\n", "import matplotlib.pyplot as plt\n", "import cartopy.crs as ccrs\n", "import cartopy.feature as cfeature\n", "from tqdm import tqdm\n", "from viresclient import SwarmRequest\n", "\n", "COLLECTIONS = {\n", " 'Swarm_1M': 'SW_OPER_VOBS_1M_2_',\n", " 'Swarm_4M': 'SW_OPER_VOBS_4M_2_',\n", " 'CHAMP_1M': 'CH_OPER_VOBS_1M_2_',\n", " 'Orsted_1M': 'OR_OPER_VOBS_1M_2_',\n", " 'Cryosat_1M': 'CR_OPER_VOBS_1M_2_',\n", " 'Composite_1M': 'CO_OPER_VOBS_1M_2_',\n", " 'Orsted_4M': 'OR_OPER_VOBS_4M_2_',\n", " 'CHAMP_4M': 'CH_OPER_VOBS_4M_2_',\n", " 'Cryosat_4M': 'CR_OPER_VOBS_4M_2_',\n", " 'Composite_4M': 'CO_OPER_VOBS_4M_2_'\n", "}\n", "\n", "def nec2rtp(ds):\n", " \"\"\"Convert NEC coords to RTP. NB: data_var names do not change\"\"\"\n", " _ds = ds.copy()\n", " # Convert variables from NEC to RTP frame\n", " for var in _ds.data_vars:\n", " if \"NEC\" in _ds[var].dims:\n", " _ds[var] = np.array([-1, 1, -1])*_ds[var]\n", " _ds[var] = _ds[var].roll({\"NEC\": 1}, roll_coords=False)\n", " _ds[var].attrs = ds[var].attrs\n", " # Rename NEC dims & coords to RTP\n", " _ds = _ds.rename({\"NEC\": \"RTP\"})\n", " _ds = _ds.assign_coords({\"RTP\": [\"Radial\", \"Theta\", \"Phi\"]})\n", " _ds[\"RTP\"].attrs = {\n", " \"units\": \"\",\n", " \"description\": \"RTP frame - Radial, Theta, Phi [R,T,P] = [-C,-N,E]\"\n", " }\n", " return _ds\n", "\n", "def fetch_vobs(collection, sv=False, reshape=True, rtp=True):\n", " \"\"\"Fetch data from VirES for a given collection\"\"\"\n", " collection = f\"{collection}:SecularVariation\" if sv else collection\n", " if sv:\n", " measurements = ['SiteCode', 'B_SV', 'sigma_SV']\n", " else:\n", " measurements = ['SiteCode', 'B_CF', 'B_OB', 'sigma_CF', 'sigma_OB']\n", " request = SwarmRequest()\n", " request.set_collection(collection)\n", " request.set_products(\n", " measurements=measurements,\n", " )\n", " data = request.get_between(\n", " dt.datetime(1999, 1, 1),\n", " dt.datetime(2024, 1, 1),\n", " asynchronous=False, show_progress=False\n", " )\n", " ds = data.as_xarray(reshape=reshape)\n", " if rtp:\n", " ds = nec2rtp(ds)\n", " return ds\n", "\n", "def merge_vobs(ds, ds_sv):\n", " \"\"\"Merge in SecularVariation data by using new 'Timestamp_SV' coord\"\"\"\n", " ds_sv = ds_sv.rename({\"Timestamp\": \"Timestamp_SV\"})\n", " # Copy metadata\n", " attrs = ds.attrs.copy()\n", " for k, v in ds_sv.attrs.items():\n", " attrs[k].extend(v)\n", " attrs[k] = list(set(attrs[k]))\n", " ds = xr.merge((ds, ds_sv))\n", " ds.attrs = attrs\n", " return ds\n", "\n", "def fetch_all_vobs():\n", " \"\"\"Gives a dictionary containing datasets, one for each VOBS collection\"\"\"\n", " ALL_VOBS = {}\n", " for key, collection in tqdm(COLLECTIONS.items()):\n", " ds = fetch_vobs(collection)\n", " ds_sv = fetch_vobs(collection, sv=True)\n", " ds = merge_vobs(ds, ds_sv)\n", " ALL_VOBS[key] = ds\n", " return ALL_VOBS\n", "\n", "ALL_VOBS = fetch_all_vobs()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Each dataset is now available within the dictionary `ALL_VOBS`, and can be accessed like:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ALL_VOBS[\"Swarm_4M\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the altitudes of the virtual observatories defined for each mission are different, so the measurements can not directly be compared. Below, we can see that CHAMP and Swarm VO's are at lower altitude so the observed field is stronger. The *composite* data product has VO's defined at a common altitude of 700km." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rad = {}\n", "for mission in (\"Orsted\", \"CHAMP\", \"Cryosat\", \"Swarm\", \"Composite\"):\n", " rad[mission] = int(ALL_VOBS[f\"{mission}_4M\"][\"Radius\"][0]/1e3)\n", "\n", "plt.figure(figsize=(10,3))\n", "for mission in (\"Orsted\", \"CHAMP\", \"Cryosat\", \"Swarm\", \"Composite\"):\n", " missionname = \"Ørsted\" if mission == \"Orsted\" else mission\n", " label = f\"{missionname} @ {rad[mission]}km\"\n", " ALL_VOBS[f\"{mission}_4M\"].sel(SiteCode=\"S77W114\", RTP=\"Radial\").plot.scatter(\n", " x=\"Timestamp\", y=\"B_OB\", label=label\n", " )\n", "plt.title(\"Radial component at S77W114 (4-monthly estimate)\")\n", "plt.xlabel(\"Time\")\n", "plt.grid()\n", "plt.legend(loc=(1.05, 0), title=\"Geocentric distance\");" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Visualising data from one location" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_vobs_data(mission, site):\n", " \"\"\"Plot a 3x3 grid of the data from a given mission at a given VO site\"\"\"\n", " fig, axes = plt.subplots(nrows=3, ncols=3, sharex=\"col\", figsize=(15, 10))\n", " available_1m = True if f\"{mission}_1M\" in ALL_VOBS.keys() else False\n", " if available_1m:\n", " ds_1m = ALL_VOBS[f\"{mission}_1M\"].sel(SiteCode=site)\n", " ds_4m = ALL_VOBS[f\"{mission}_4M\"].sel(SiteCode=site)\n", " for i, rtp in enumerate((\"Radial\", \"Theta\", \"Phi\")):\n", " if available_1m:\n", " _ds_1m = ds_1m.sel(RTP=rtp)\n", " # Observed field\n", " axes[i, 0].errorbar(\n", " _ds_1m[\"Timestamp\"].values, _ds_1m[\"B_OB\"].values, np.abs(_ds_1m[\"sigma_OB\"].values),\n", " fmt=\".\", label=\"1M\"\n", " )\n", " # Core field\n", " axes[i, 1].errorbar(\n", " _ds_1m[\"Timestamp\"].values, _ds_1m[\"B_CF\"].values, np.abs(_ds_1m[\"sigma_CF\"].values),\n", " fmt=\".\",\n", " )\n", " # Secular variation (of core field)\n", " axes[i, 2].errorbar(\n", " _ds_1m[\"Timestamp_SV\"].values, _ds_1m[\"B_SV\"].values, np.abs(_ds_1m[\"sigma_SV\"].values),\n", " fmt=\".\",\n", " )\n", " _ds_4m = ds_4m.sel(RTP=rtp)\n", " # Observed field\n", " axes[i, 0].errorbar(\n", " _ds_4m[\"Timestamp\"].values, _ds_4m[\"B_OB\"].values, np.abs(_ds_4m[\"sigma_OB\"].values),\n", " fmt=\".\", label=\"4M\"\n", " )\n", " # Core field\n", " axes[i, 1].errorbar(\n", " _ds_4m[\"Timestamp\"].values, _ds_4m[\"B_CF\"].values, np.abs(_ds_4m[\"sigma_CF\"].values),\n", " fmt=\".\",\n", " )\n", " axes[i, 1].set_ylim(axes[i, 0].get_ylim())\n", " # Secular variation (of core field)\n", " axes[i, 2].errorbar(\n", " _ds_4m[\"Timestamp_SV\"].values, _ds_4m[\"B_SV\"].values, np.abs(_ds_4m[\"sigma_SV\"].values),\n", " fmt=\".\",\n", " )\n", " axes[0, 0].set_ylabel(\"Radial\\n[nT]\")\n", " axes[1, 0].set_ylabel(\"Theta\\n[nT]\")\n", " axes[2, 0].set_ylabel(\"Phi\\n[nT]\")\n", " for i in (0, 1, 2):\n", " axes[i, 1].set_ylabel(\"[nT]\")\n", " axes[i, 2].set_ylabel(\"[nT/yr]\")\n", " axes[0, 0].set_title(\"B_OB (Observed field)\")\n", " axes[0, 1].set_title(\"B_CF (Core field)\")\n", " axes[0, 2].set_title(\"B_SV (Secular variation)\")\n", " axes[0, 0].legend()\n", " fig.tight_layout()\n", " fig.suptitle(site, va=\"bottom\", y=1, fontsize=15)\n", " return fig, axes\n", "\n", "plot_vobs_data(\"Swarm\", \"N65W051\");" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Visualising data from all locations (using cartopy)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "LINE_COLORS = {\n", " \"Radial\": mpl.colors.to_hex(\"tab:blue\"),\n", " \"Theta\": mpl.colors.to_hex(\"tab:orange\"),\n", " \"Phi\": mpl.colors.to_hex(\"tab:green\"),\n", "}\n", "\n", "TITLES = {\n", " \"Radial\": r\"dB$_r$ / dt\",\n", " \"Theta\": r\"dB$_\\theta$ / dt\",\n", " \"Phi\": r\"dB$_\\phi$ / dt\"\n", "}\n", "\n", "def make_sv_map(ds, RTP=\"Radial\", var=\"B_SV\"):\n", " \"\"\"Generate overview map of SV from a given dataset\"\"\"\n", " # Set up underlying map to plot onto\n", " fig = plt.figure(figsize=(16, 8), dpi=150)\n", " ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(),extent=[-180, 180, -90, 90])\n", " ax.add_feature(cfeature.LAND, color=\"lightgrey\")\n", " ax.gridlines(draw_labels=True, dms=True, x_inline=False, y_inline=False)\n", " GVO_LOCATIONS = np.vstack((ds[\"Longitude\"].values, ds[\"Latitude\"].values)).T\n", " ax.scatter(*GVO_LOCATIONS.T, color=\"black\", s=3, zorder=2)\n", " # Add fractional year to use as x-axis\n", " tvar = \"Timestamp_SV\" if var == \"B_SV\" else \"Timestamp\"\n", " ds[\"Year\"] = ds[tvar].dt.year + ds[tvar].dt.month/12\n", " ds = ds.set_coords(\"Year\")\n", " # Min and max values to use for scaling\n", " min_y = np.nanmin(ds[var].sel(RTP=RTP).data)\n", " max_y = np.nanmax(ds[var].sel(RTP=RTP).data)\n", " min_x = np.nanmin(ds[\"Year\"].data)\n", " max_x = np.nanmax(ds[\"Year\"].data)\n", " scale_x = 10\n", " scale_y = 40\n", " # Loop through each GVO\n", " for i in range(300):\n", " # Extract data for only that GVO and vector component\n", " gvo_record = ds.sel(RTP=RTP).isel(SiteCode=i)\n", " # Get x & y values and scale them, centred around the GVO location\n", " # Scale values between 0 & 1:\n", " x = (gvo_record[\"Year\"].data - min_x) / (max_x - min_x)\n", " y = (gvo_record[var].data - min_y) / (max_y - min_y)\n", " # Shift values to centre around the lat/lon position:\n", " lat = float(gvo_record[\"Latitude\"])\n", " lon = float(gvo_record[\"Longitude\"])\n", " x = lon + scale_x*(x - 0.5)\n", " y = lat + scale_y*(y - np.nanmean(y))\n", " # Plot these points onto the figure\n", " gvo_xy_verts = np.vstack((x, y))\n", " ax.scatter(\n", " *gvo_xy_verts, transform=ccrs.PlateCarree(),\n", " color=LINE_COLORS.get(RTP), alpha=0.8, s=1\n", " )\n", " # Create scale indicator\n", " dx = scale_x\n", " dy = scale_y * 20 / (max_y-min_y)\n", " p_x = 160\n", " p_y = 105\n", " ax.arrow(p_x, p_y, dx, 0, linewidth=2, head_width=0).set_clip_on(False)\n", " ax.arrow(p_x, p_y, 0, dy, linewidth=2, head_width=0).set_clip_on(False)\n", " ax.text(p_x-2, p_y+dx/2, \"20nT/yr\", va=\"top\", ha=\"right\")\n", " minyr = str(np.round(min_x, 1))\n", " maxyr = str(np.round(max_x, 1))\n", " ax.text(p_x, p_y-2, f\"{minyr} - {maxyr}\", va=\"top\", ha=\"left\")\n", " fig.suptitle(TITLES[RTP], fontsize=15)\n", " return fig, ax\n", "\n", "fig, ax = make_sv_map(ALL_VOBS[\"Swarm_4M\"], RTP=\"Radial\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can now repeat this graph, changing the key to the `ALL_VOBS` dictionary to one of:\n", "```\n", "'Swarm_1M', 'Swarm_4M', 'CHAMP_1M', 'Orsted_1M', 'Cryosat_1M', 'Composite_1M', 'Orsted_4M', 'CHAMP_4M', 'Cryosat_4M', 'Composite_4M'\n", "```\n", "and `RTP` to one of `'Radial', 'Theta', 'Phi'`. For example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = make_sv_map(ALL_VOBS[\"Composite_1M\"], RTP=\"Phi\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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" } }, "nbformat": 4, "nbformat_minor": 4 }