{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# AEJxLPL (Auroral electrojets LC)\n", "\n", "> Abstract: Access to the AEBS products (\"Auroral Electrojet and auroral Boundaries estimated from Swarm observations\"). These are a family of L2 (fast-track) products: `AEJxLPL`, `AEJxLPS`, `AEJxPBL`, `AEJxPBS`, `AOBxFAC`. The products provide latitude profiles of auroral electrojet currents from two methods - the \"line current\" (LC) and \"Spherical Elementary Current Systems\" (SECS) - as well as the locations of current maxima and minima and the boundaries of the current systems determined by each method, and the auroral oval boundaries determined from FAC observations." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "%load_ext watermark\n", "%watermark -i -v -p viresclient,pandas,xarray,matplotlib" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "from viresclient import SwarmRequest\n", "import datetime as dt\n", "import numpy as np\n", "import pandas as pd\n", "import xarray as xr\n", "import matplotlib.pyplot as plt\n", "import matplotlib as mpl\n", "\n", "request = SwarmRequest()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## AEBS product information\n", "\n", "These comprise five products: \n", "`AEJxLPL`: Auroral electrojets (AEJ) - Latitude profiles (LP) - Line current method (L) \n", "`AEJxLPS`: Auroral electrojets (AEJ) - Latitude profiles (LP) - SECS method (S) \n", "`AEJxPBL`: Auroral electrojets (AEJ) - Peaks and boundaries (PB) - Line current method (L) \n", "`AEJxPBS`: Auroral electrojets (AEJ) - Peaks and boundaries (PB) - SECS method (S) \n", "`AOBxFAC`: Auroral oval boundaries (AOB) - from FAC method \n", "\n", "These products are updated on a daily basis through the fast-track Swarm processing chain and are defined along the orbits of each Swarm spacecraft.\n", "\n", "The LC method produces only horizontal sheet current density (2D) while the SECS method produces both the horizontal sheet current density and the radial current density (3D) as well as a prediction for the satellite ground track location experiencing the greatest magnetic disturbance.\n", "\n", "*For the `LPL` (line current) products:* \n", "Horizontal current densities are given both in the NEC (*geographic*) frame (`J_NE` - North/East components) and the QD (*quasi-dipole*) frame (`J_QD`).\n", "\n", "*For the `LPS` (SECS) products:* \n", "The horizontal current densities are decomposed into *curl-free* (CF) and *divergence-free* (DF) parts and expressed in both the NEC and semi-QD frames: `J_CF_NE`, `J_DF_NE`, `J_CF_SemiQD`, `J_DF_SemiQD`. The radial current density is also given in the semi-QD frame: `J_R`.\n", "\n", "The `PBL` and `PBS` products contain the current system peaks and boundaries determined from each of `LPL` and `LPS` respectively. The `PBS` product also contains a prediction for the location and size of the peak ground magnetic disturbance.\n", "\n", "Documentation:\n", "- Not yet public but should appear at https://earth.esa.int/web/guest/missions/esa-eo-missions/swarm/data-handbook/level-2-product-definitions\n", "- ref SW‐DS‐DTU‐GS‐003_AEBS_PDD\n", "\n", "Details on implementation in VirES and more demos:\n", "- https://github.com/pacesm/jupyter_notebooks/tree/master/AEBS\n", "\n", "The layout of these data is quite complex and the original data are mapped to variables within VirES as detailed in: \n", "https://github.com/pacesm/jupyter_notebooks/blob/master/AEBS/AEBS_00_data_access.ipynb\n", "\n", "You can also see an overview of the collection and parameter naming in VirES at https://viresclient.readthedocs.io/en/latest/available_parameters.html#collections" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Example with the AEJALPL: J_QD time series" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Check available data variables (AEJ_LPL, AEJ_LPL:Quality, and AEJ_PBL product type)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "request.available_collections(\"AEJ_LPL\", details=False)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "request.available_measurements(\"SW_OPER_AEJALPL_2F\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "request.available_collections(\"AEJ_LPL:Quality\", details=False)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "request.available_measurements(\"SW_OPER_AEJALPL_2F:Quality\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "request.available_collections(\"AEJ_PBL\", details=False)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "request.available_measurements(\"SW_OPER_AEJAPBL_2F\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Fetch the three collections: LPL, LPL:Quality, and PBL" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "start_time = dt.datetime(2015, 6, 1)\n", "end_time = dt.datetime(2015, 6, 8)\n", "spacecraft = 'A'\n", "auxiliaries = ['OrbitNumber', 'QDLat', 'QDOrbitDirection', 'MLT', 'Kp']\n", "\n", "# Fetch LPL\n", "request.set_collection(f'SW_OPER_AEJ{spacecraft}LPL_2F')\n", "request.set_products(\n", " measurements=['J_NE', 'J_QD'],\n", " auxiliaries=auxiliaries\n", ")\n", "data = request.get_between(start_time, end_time)\n", "ds_lpl = data.as_xarray()\n", "# Fetch LPL Quality\n", "request.set_collection(f'SW_OPER_AEJ{spacecraft}LPL_2F:Quality')\n", "request.set_products(\n", " measurements=['RMS_misfit', 'Confidence'],\n", ")\n", "data = request.get_between(start_time, end_time)\n", "ds_lplq = data.as_xarray()\n", "# Fetch PBL\n", "request.set_collection(f'SW_OPER_AEJ{spacecraft}PBL_2F')\n", "request.set_products(\n", " measurements=['PointType', 'Flags'],\n", " auxiliaries=auxiliaries\n", ")\n", "data = request.get_between(start_time, end_time)\n", "ds_pbl = data.as_xarray()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "print(\"ds_lpl:\\n\", ds_lpl, \"\\n\")\n", "print(\"ds_lplq:\\n\", ds_lplq, \"\\n\")\n", "print(\"ds_pbl:\\n\", ds_pbl)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Reconstruct `ds_pbl` into a more manageable form\n", "Split `PointType` apart into separate boolean arrays, one for each point type\n", "\n", "For more info on `PointType`, see:\n", "https://nbviewer.jupyter.org/github/pacesm/jupyter_notebooks/blob/master/AEBS/AEBS_00_data_access.ipynb#AEJxPBL-product" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Meaning of PointType\n", "PointType_meanings = {\n", " \"WEJ_peak\": 0, # minimum\n", " \"EEJ_peak\": 1, # maximum\n", " \"WEJ_eq_bound_s\": 2, # equatorward (pair start)\n", " \"EEJ_eq_bound_s\": 3,\n", " \"WEJ_po_bound_s\": 6, # poleward\n", " \"EEJ_po_bound_s\": 7,\n", " \"WEJ_eq_bound_e\": 10, # equatorward (pair end)\n", " \"EEJ_eq_bound_e\": 11,\n", " \"WEJ_po_bound_e\": 14, # poleward\n", " \"EEJ_po_bound_e\": 15,\n", "}\n", "# Add new data variables (boolean Type) according to the dictionary above\n", "ds_pbl = ds_pbl.assign(\n", " {name: ds_pbl[\"PointType\"] == PointType_meanings[name]\n", " for name in PointType_meanings.keys()}\n", ")\n", "ds_pbl" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Merge (`ds_lpl`, `ds_pbl`) into one `ds`\n", "\n", "`ds_pbl` contains duplicate Timestamp entries because some points in the time series contain more than one identified PointType. This is a problem for straightforward merging with `ds_lpl`. This can be worked around with this strategy:\n", "1. Create a new dataset from the newly created PointType arrays, without the repeating timestamps\n", "2. Merge this with the LPL dataset - this is an outer merge since the PBL contains timestamps that don't appear in the LPL data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def drop_duplicate_times(_ds):\n", " _, index = np.unique(_ds['Timestamp'], return_index=True)\n", " return _ds.isel(Timestamp=index)\n", "\n", "def merge_attrs(_ds1, _ds2):\n", " attrs = {\"Sources\":[], \"MagneticModels\":[], \"AppliedFilters\":[]}\n", " for item in [\"Sources\", \"MagneticModels\", \"AppliedFilters\"]:\n", " attrs[item] = list(set(_ds1.attrs[item] + _ds2.attrs[item]))\n", " return attrs\n", "\n", "# Create new dataset from just the newly created PointType arrays\n", "# This is created on a non-repeating Timestamp coordinate\n", "ds = xr.Dataset(\n", " {name: ds_pbl[name].where(ds_pbl[name], drop=True)\n", " for name in PointType_meanings.keys()}\n", ")\n", "# Merge in the positional and auxiliary data\n", "data_vars = list(set(ds_pbl.data_vars).difference(set(PointType_meanings.keys())))\n", "data_vars.remove(\"PointType\")\n", "ds = ds.merge(\n", " (ds_pbl[data_vars]\n", " .pipe(drop_duplicate_times))\n", ")\n", "# Merge together with the LPL data\n", "# Note that the Timestamp coordinates aren't equal\n", "\n", "# Separately merge data with matching and missing time sample points in ds_lpl\n", "idx_present = list(set(ds[\"Timestamp\"].values).intersection(set(ds_lpl[\"Timestamp\"].values)))\n", "idx_missing = list(set(ds[\"Timestamp\"].values).difference(set(ds_lpl[\"Timestamp\"].values)))\n", "# Override prioritises the first dataset (ds_lpl) where there are conflicts\n", "ds2 = ds_lpl.merge(ds.sel(Timestamp=idx_present), join=\"outer\", compat=\"override\")\n", "ds2 = ds2.merge(ds.sel(Timestamp=idx_missing), join=\"outer\")\n", "# Update the metadata\n", "ds2.attrs = merge_attrs(ds_lpl, ds_pbl)\n", "\n", "# Switch the point type arrays to uint8 or bool for performance?\n", "# But the .where operations later cast them back to float64 since gaps are filled with nan\n", "for name in PointType_meanings.keys():\n", " ds2[name] = ds2[name].astype(\"uint8\").fillna(False)\n", " # ds2[name] = ds2[name].fillna(False).astype(bool)\n", "\n", "ds = ds2\n", "ds" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can extract locations within `ds` where a certain point type is identified, and plot them:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "(\n", " ds.where(ds[\"EEJ_peak\"], drop=True)\n", " .plot.scatter(y=\"Latitude\", x=\"Longitude\")# hue=\"MLT\", cmap=\"twilight_shifted\")\n", ");" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Append the PBL Flags information into the LPL:Quality dataset to use as a lookup table" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ds_lplq = ds_lplq.assign(\n", " Flags_PBL=\n", " ds_pbl[\"Flags\"]\n", " .pipe(drop_duplicate_times)\n", " .reindex_like(ds_lplq, method=\"nearest\"),\n", ")\n", "ds_lplq\n", "\n", "## A particular time can be searched for like:\n", "# t = ds_lpl[\"Timestamp\"].isel(Timestamp=0).values\n", "# ds_lplq.sel(Timestamp=t, method=\"nearest\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Overview of the week accessed\n", "\n", "NB the coloured plot suffers from overplotting lines as we have not separated the ascending/descending sectors of the passes over the poles." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, axes = plt.subplots(nrows=3, ncols=1, sharex=True, figsize=(10, 8))\n", "axes[0].plot(ds[\"Timestamp\"], ds[\"J_QD\"])\n", "axes[0].set_ylabel(\"J_QD\")\n", "cmap = mpl.cm.RdBu\n", "norm = plt.Normalize(-200, 200)\n", "axes[1].scatter(\n", " x=ds[\"Timestamp\"].values, y=ds[\"Latitude\"].values, c=ds[\"J_QD\"].values,\n", " s=1, cmap=cmap, norm=norm\n", ")\n", "ax_cbar = fig.add_axes([0.95, 0.4, 0.02, 0.2])\n", "mpl.colorbar.ColorbarBase(\n", " ax_cbar, cmap=cmap, norm=norm, orientation=\"vertical\", label=\"J_QD\"\n", ")\n", "axes[1].set_ylabel(\"Latitude\")\n", "_da_kp = ds[\"Kp\"].dropna(dim=\"Timestamp\")\n", "axes[2].plot(_da_kp[\"Timestamp\"], _da_kp)\n", "axes[2].set_ylabel(\"Kp\");" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Orbit-by-orbit plot of one day" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Bit numbers which indicate non-nominal state\n", "# Check SW-DS-DTU-GS-003_AEBS_PDD for details\n", "BITS_PBL_FLAGS_EEJ_MINOR = (2, 3, 6)\n", "BITS_PBL_FLAGS_WEJ_MINOR = (4, 5, 6)\n", "BITS_PBL_FLAGS_EEJ_BAD = (0, 7, 8, 11)\n", "BITS_PBL_FLAGS_WEJ_BAD = (1, 9, 10, 12)\n", "\n", "def check_PBL_Flags(flags=0b0, EJ_type=\"WEJ\"):\n", " \"\"\"Return \"good\", \"poor\", or \"bad\" depending on status\"\"\"\n", " def _check_bits(bitno_set):\n", " return any(flags & (1 << bitno) for bitno in bitno_set)\n", " if EJ_type == \"WEJ\":\n", " if _check_bits(BITS_PBL_FLAGS_WEJ_BAD):\n", " return \"bad\"\n", " elif _check_bits(BITS_PBL_FLAGS_WEJ_MINOR):\n", " return \"poor\"\n", " else:\n", " return \"good\"\n", " elif EJ_type == \"EEJ\":\n", " if _check_bits(BITS_PBL_FLAGS_EEJ_BAD):\n", " return \"bad\"\n", " elif _check_bits(BITS_PBL_FLAGS_EEJ_MINOR):\n", " return \"poor\"\n", " else:\n", " return \"good\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "glyphs = {\n", " \"WEJ_peak\": {\"marker\": 'v', \"color\":'tab:red'}, # minimum\n", " \"EEJ_peak\": {\"marker\": '^', \"color\":'tab:purple'}, # maximum\n", " \"WEJ_eq_bound_s\": {\"marker\": '>', \"color\":'black'}, # equatorward (pair start)\n", " \"EEJ_eq_bound_s\": {\"marker\": '>', \"color\":'black'},\n", " \"WEJ_po_bound_s\": {\"marker\": '>', \"color\":'black'}, # poleward\n", " \"EEJ_po_bound_s\": {\"marker\": '>', \"color\":'black'},\n", " \"WEJ_eq_bound_e\": {\"marker\": '<', \"color\":'black'}, # equatorward (pair end)\n", " \"EEJ_eq_bound_e\": {\"marker\": '<', \"color\":'black'},\n", " \"WEJ_po_bound_e\": {\"marker\": '<', \"color\":'black'}, # poleward\n", " \"EEJ_po_bound_e\": {\"marker\": '<', \"color\":'black'},\n", "}\n", "\n", "def plot_stack(ds, hemisphere=\"North\"):\n", " if hemisphere == \"North\":\n", " ds = ds.where(ds[\"Latitude\"]>0, drop=True)\n", " elif hemisphere == \"South\":\n", " ds = ds.where(ds[\"Latitude\"]<0, drop=True)\n", " # Generate plot with split by columns: ascending/descending to/from pole\n", " # by rows: successive orbits\n", " # Skip when no data available\n", " if len(ds[\"Timestamp\"]) == 0:\n", " return None, None\n", " fig, axes = plt.subplots(\n", " nrows=len(ds.groupby(\"OrbitNumber\")), ncols=2, sharex=\"col\", sharey=\"all\",\n", " figsize=(10, 20)\n", " )\n", " max_ylim = np.max(np.abs(ds[\"J_QD\"]))\n", " # Loop through each orbit\n", " for i, (_, ds_orbit) in enumerate(ds.groupby(\"OrbitNumber\")):\n", " if hemisphere == \"North\":\n", " ds_orb_asc = ds_orbit.where(ds_orbit[\"QDOrbitDirection\"] == 1, drop=True)\n", " ds_orb_desc = ds_orbit.where(ds_orbit[\"QDOrbitDirection\"] == -1, drop=True)\n", " if hemisphere == \"South\":\n", " ds_orb_asc = ds_orbit.where(ds_orbit[\"QDOrbitDirection\"] == -1, drop=True)\n", " ds_orb_desc = ds_orbit.where(ds_orbit[\"QDOrbitDirection\"] == 1, drop=True)\n", " # Loop through ascending and descending sections\n", " for j, _ds in enumerate((ds_orb_asc, ds_orb_desc)):\n", " # Line plot of current strength\n", " axes[i, j].plot(\n", " _ds[\"QDLat\"], _ds[\"J_QD\"],\n", " color=\"tab:blue\", marker=\".\", linestyle=\"\"\n", " )\n", " # Plot glyphs at the peaks and boundaries locations\n", " for name in glyphs.keys():\n", " __ds = _ds.where(_ds[name], drop=True)\n", " try:\n", " for qdlat in __ds[\"QDLat\"]:\n", " axes[i, j].plot(\n", " qdlat, 0,\n", " marker=glyphs[name][\"marker\"], color=glyphs[name][\"color\"]\n", " )\n", " except Exception:\n", " pass\n", " # Identify Quality and Flags info\n", " # Use either the start time of the section or the end, depending on asc or desc\n", " index = 0 if j == 0 else -1\n", " t = _ds[\"Timestamp\"].isel(Timestamp=index).values\n", " _ds_qualflags = ds_lplq.sel(Timestamp=t, method=\"nearest\")\n", " pbl_flags = int(_ds_qualflags[\"Flags_PBL\"].values)\n", " lpl_rms_misfit = float(_ds_qualflags[\"RMS_misfit\"].values)\n", " lpl_confidence = float(_ds_qualflags[\"Confidence\"].values)\n", " # Shade WEJ and EEJ regions, only if well-defined\n", " # def _shade_EJ_region(_ds=None, EJ=\"WEJ\", color=\"tab:red\", alpha=0.3):\n", " wej_status = check_PBL_Flags(pbl_flags, \"WEJ\")\n", " eej_status = check_PBL_Flags(pbl_flags, \"EEJ\")\n", " if wej_status in [\"good\", \"poor\"]:\n", " alpha = 0.3 if wej_status == \"good\" else 0.1\n", " try:\n", " WEJ_left = _ds.where(\n", " (_ds[\"WEJ_eq_bound_s\"] == 1) | (_ds[\"WEJ_po_bound_s\"] == 1), drop=True)\n", " WEJ_right = _ds.where(\n", " (_ds[\"WEJ_eq_bound_e\"] == 1) | (_ds[\"WEJ_po_bound_e\"] == 1), drop=True)\n", " x1 = WEJ_left[\"QDLat\"][0]\n", " x2 = WEJ_right[\"QDLat\"][0]\n", " axes[i, j].fill_betweenx(\n", " [-max_ylim, max_ylim], [x1, x1], [x2, x2], color=\"tab:red\", alpha=alpha)\n", " except Exception:\n", " pass\n", " if eej_status in [\"good\", \"poor\"]:\n", " alpha = 0.3 if eej_status == \"good\" else 0.15\n", " try:\n", " EEJ_left = _ds.where(\n", " (_ds[\"EEJ_eq_bound_s\"] == 1) | (_ds[\"EEJ_po_bound_s\"] == 1), drop=True)\n", " EEJ_right = _ds.where(\n", " (_ds[\"EEJ_eq_bound_e\"] == 1) | (_ds[\"EEJ_po_bound_e\"] == 1), drop=True)\n", " x1 = EEJ_left[\"QDLat\"][0]\n", " x2 = EEJ_right[\"QDLat\"][0]\n", " axes[i, j].fill_betweenx(\n", " [-max_ylim, max_ylim], [x1, x1], [x2, x2], color=\"tab:purple\", alpha=alpha)\n", " except Exception:\n", " pass\n", " # Write the LPL:Quality and PBL Flags info\n", " ha = \"right\" if j == 0 else \"left\"\n", " textx = 0.98 if j == 0 else 0.02\n", " axes[i, j].text(\n", " textx, 0.95,\n", " f\"RMS Misfit {np.round(lpl_rms_misfit, 2)}; Confidence {np.round(lpl_confidence, 2)}\",\n", " transform=axes[i, j].transAxes, verticalalignment=\"top\", horizontalalignment=ha\n", " )\n", " axes[i, j].text(\n", " textx, 0.05,\n", " f\"PBL Flags {pbl_flags:013b}\",\n", " transform=axes[i, j].transAxes, verticalalignment=\"bottom\", horizontalalignment=ha\n", " )\n", " # Write the start/end time and MLT of the section, and the orbit number\n", " def _format_utc(t):\n", " return f\"UTC {t.strftime('%H:%M')}\"\n", " def _format_mlt(mlt):\n", " hour, fraction = divmod(mlt, 1)\n", " t = dt.time(int(hour), minute=int(60*fraction))\n", " return f\"MLT {t.strftime('%H:%M')}\"\n", " # Left part (section starting UTC, MLT, OrbitNumber)\n", " time_s = pd.to_datetime(ds_orb_asc[\"Timestamp\"].isel(Timestamp=0).data)\n", " mlt_s = ds_orb_asc[\"MLT\"].dropna(dim=\"Timestamp\").isel(Timestamp=0).data\n", " orbit_number = int(ds_orb_asc[\"OrbitNumber\"].isel(Timestamp=0).data)\n", " axes[i, 0].text(\n", " 0.01, 0.95, f\"{_format_utc(time_s)}\\n{_format_mlt(mlt_s)}\",\n", " transform=axes[i, 0].transAxes, verticalalignment=\"top\"\n", " )\n", " axes[i, 0].text(\n", " 0.01, 0.05, f\"Orbit {orbit_number}\",\n", " transform=axes[i, 0].transAxes, verticalalignment=\"bottom\"\n", " )\n", " # Right part (section ending UTC, MLT)\n", " time_e = pd.to_datetime(ds_orb_desc[\"Timestamp\"].isel(Timestamp=-1).data)\n", " mlt_e = ds_orb_desc[\"MLT\"].dropna(dim=\"Timestamp\").isel(Timestamp=-1).data\n", " axes[i, 1].text(\n", " 0.99, 0.95, f\"{_format_utc(time_e)}\\n{_format_mlt(mlt_e)}\",\n", " transform=axes[i, 1].transAxes, verticalalignment=\"top\", horizontalalignment=\"right\"\n", " )\n", " # Extra config of axes and figure text\n", " axes[0, 0].set_ylim(-max_ylim, max_ylim)\n", " if hemisphere == \"North\":\n", " axes[0, 0].set_xlim(50, 90)\n", " axes[0, 1].set_xlim(90, 50)\n", " elif hemisphere == \"South\":\n", " axes[0, 0].set_xlim(-50, -90)\n", " axes[0, 1].set_xlim(-90, -50)\n", " for ax in axes.flatten():\n", " ax.grid()\n", " axes[-1, 0].set_xlabel(\"QD Latitude\")\n", " axes[-1, 0].set_ylabel(\"Eastward electrojet\\n[ A.km$^{-1}$ ]\")\n", " time = pd.to_datetime(ds[\"Timestamp\"].isel(Timestamp=0).data)\n", " spacecraft = ds[\"Spacecraft\"].dropna(dim=\"Timestamp\").isel(Timestamp=0).data\n", " fig.text(\n", " 0.5, 0.9, f\"{time.strftime('%Y-%m-%d')}\\nSwarm {spacecraft}\\n{hemisphere}\",\n", " transform=fig.transFigure, horizontalalignment=\"center\",\n", " )\n", " fig.subplots_adjust(wspace=0, hspace=0)\n", " return fig, axes\n", "\n", "def plot_day(ds, day=\"20150101\", hemisphere=\"North\"):\n", " ds = ds.sel(Timestamp=day)\n", " fig, axes = plot_stack(ds, hemisphere)\n", " return fig, axes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`plot_stack()` can be used to build the plot from a given dataset, separately for each hemisphere. `plot_day()` selects and plots a particular day.\n", "\n", "Consecutive orbits are shown in consecutive rows, centred over the magnetic pole (defined by quasi-dipole latitude). The starting and ending times (UTC and MLT) of the orbital section are shown at the left and right. Westward (WEJ) and Eastward (EEJ) electrojet extents and peak intensities are indicated:\n", "- Blue dots: Estimated current density, J\n", "- Red/Purple shaded region: WEJ/EEJ extent (boundaries marked by black triangles)\n", "- Red/Purple triangles: Locations of peak WEJ/EEJ intensity" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "fig, axes = plot_day(ds, day=\"20150601\", hemisphere=\"North\")" ] } ], "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 }