Source code for pyrfu.pyrf.mva_gui

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# 3rd party imports
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import dates, gridspec
from matplotlib.widgets import Button, SpanSelector

# Local imports
from ..plot import plot_line
from .mva import mva
from .new_xyz import new_xyz
from .norm import norm
from .time_clip import time_clip

__author__ = "Atlas Silverhult"
__email__ = "atlas.silverhult.9977@student.uu.se"
__copyright__ = "Copyright 2020-2023"
__license__ = "MIT"
__version__ = "2.4.2"
__status__ = "Prototype"

__all__ = ["mva_gui"]


[docs]def mva_gui(inp): r"""GUI to interactively perform minimum variance analysis (MVA) on time series data by selecting the time interval to apply MVA on. The return of this function is a callback to the GUI object and class attributes like the minimum variance direction vector are accesable through this callback by the method get_minvar(). Parameters ---------- inp : xarray.DataArray Time series of the quantity to load into GUI and perform MVA on. Returns ------- mva_callback : Returns MvaGui object to access attributes. In order to keep GUI responsive and interactive, a reference to this object is needed. """ mva_callback = MvaGui(inp) return mva_callback
class MvaGui: r"""Class to display and update GUI elements of MVA.""" def __init__(self, b): # Time series data self.b = b self.t = self.b.time.data # Figure and GUI self.fig = self.init_fig(self.b) axreset = self.fig.add_axes([0.91, 0.66, 0.04, 0.03]) self.resetbutton = Button(axreset, "Reset", hovercolor="0.75") self.resetbutton.on_clicked(self.reset_selection) self.span = SpanSelector( self.fig.axes[0], self.update_onselect, "horizontal", useblit=True, props={"alpha": 0, "facecolor": "black"}, interactive=True, minspan=0, drag_from_anywhere=True, ) # Normal vec - to be calculated self.minvar = None self.errors = None @staticmethod def init_fig(b_xyz): r"""Initialize figure window with time series. Returns figure object to access the axes. """ fig = plt.figure(figsize=(10, 8)) legend_options = { "ncol": 1, "loc": "center left", "frameon": True, "framealpha": 1, "bbox_to_anchor": (1, 0.5), } gs = gridspec.GridSpec( 3, 3, top=0.9, left=0.05, right=0.9, hspace=0.3, wspace=0.35 ) # Time series data ax1 = fig.add_subplot(gs[0, :]) # MVA transformed frame fig.add_subplot(gs[1, :]) # Min/Max hodogram fig.add_subplot(gs[2, 0]) # Interm/Max hodogram fig.add_subplot(gs[2, 1]) # Text display ax5 = fig.add_subplot(gs[2, 2]) ax5.axis("off") # Plot b_xyz in the first axis plot_line(ax1, b_xyz) plot_line(ax1, norm(b_xyz), color="black") ax1.set_ylabel("$B$ [nT]") ax1.set_title("Time series for magnetic field") b_labels = ["$B_x$", "$B_y$", "$B_z$", "$|B|$"] ax1.legend(b_labels, **legend_options) return fig def reset_selection(self): r"""Resets MVA selection. Event is passed on from button click but has no meaning or use here. """ ax3 = self.fig.axes[2] ax4 = self.fig.axes[3] ax5 = self.fig.axes[4] self.span.set_visible(False) self.update_fig(self.b) ax3.clear() ax4.clear() for txt in ax5.texts: txt.set_visible(False) plt.draw() def update_onselect(self, tmin, tmax): r"""This function is called when a span is selected with spanselector.""" indmin, indmax = np.searchsorted(dates.date2num(self.t), (tmin, tmax)) indmax = min(len(self.t) - 1, indmax) region_t = self.t[indmin:indmax] # convert times to datetime64 tmin = np.datetime64(dates.num2date(tmin).replace(tzinfo=None)) tmax = np.datetime64(dates.num2date(tmax).replace(tzinfo=None)) b_xyz_clip = time_clip(self.b, [tmin, tmax]) if len(region_t) >= 2: self.update_fig(b_xyz_clip) self.fig.canvas.draw_idle() @staticmethod def force_positive(mva_frame, b_xyz): r"""Force maximum variance direction to be positive.""" frame = mva_frame[2] frame[:, 0] *= np.sign(max(frame[:, 0], key=abs)) # Keep frame right-handed frame[:, 2] = np.cross(frame[:, 0], frame[:, 1]) b_lmn = new_xyz(b_xyz, frame) return b_lmn, mva_frame[1], frame def update_fig(self, b_xyz_clip): r"""Update figure based on the clipped time series""" legend_options = { "ncol": 1, "loc": "center left", "frameon": True, "framealpha": 1, "bbox_to_anchor": (1, 0.5), } # Perform MVA on the clipped time series b_lmn_clip, lamb_clip, frame_clip = self.force_positive( mva(b_xyz_clip), b_xyz_clip ) # Pick out time series for each component of the new magnetic field b_1 = b_lmn_clip[:, 0] b_2 = b_lmn_clip[:, 1] b_3 = b_lmn_clip[:, 2] # Pick out each eigenvector as the columns of frame_clip v1 = frame_clip[:, 0] v2 = frame_clip[:, 1] v3 = frame_clip[:, 2] # Define axse of figure ax2 = self.fig.axes[1] ax3 = self.fig.axes[2] ax4 = self.fig.axes[3] ax5 = self.fig.axes[4] # Clear axis graphs from previous selection ax2.clear() ax3.clear() ax4.clear() # Plot b in MVA frame given by frame_clip b_new = new_xyz(self.b, frame_clip) plot_line(ax2, b_new) plot_line(ax2, norm(b_new), color="black") b2_labels = ["max", "interm", "min", "abs"] ax2.legend(b2_labels, **legend_options) ax2.set_ylabel("$B$ [nT]") ax2.set_title("MVA frame") # Update B3/B1 hodogram ax3.axis("equal") ax3.plot(b_3, b_1, c="black") ax3.set_ylabel("max") ax3.set_xlabel("min") # Update B2/B1 hodogram ax4.axis("equal") ax4.plot(b_2, b_1, c="black") ax4.set_ylabel("max") ax4.set_xlabel("interm") # Text for txt in ax5.texts: txt.set_visible(False) val_textstring = ( f"$\\lambda_1$ = {np.round(lamb_clip[0], 2)}\n" f"$\\lambda_2$ = {np.round(lamb_clip[1], 2)}\n" f"$\\lambda_3$ = {np.round(lamb_clip[2], 2)}" ) ratio_textstring = ( f"\n" f"$\\lambda_1 / \\lambda_2$ =" f" {np.round(lamb_clip[0] / lamb_clip[1], 1)}\n" f"$\\lambda_2 / \\lambda_3$ = {np.round(lamb_clip[1] / lamb_clip[2], 1)}" ) vec_textstring = ( f"\n" f"$x_1$ = {np.round(v1, 2)}\n" f"$x_2$ = {np.round(v2, 2)}\n" f"$x_3$ = {np.round(v3, 2)}" ) fontsize = "large" ax5.text( 0, 1, val_textstring, fontsize=fontsize, ha="left", va="top", transform=ax5.transAxes, ) ax5.text( 0, 0.5, ratio_textstring, fontsize=fontsize, ha="left", va="center", transform=ax5.transAxes, ) ax5.text( 0, 0, vec_textstring, fontsize=fontsize, ha="left", va="center", transform=ax5.transAxes, ) # Set minimum variance vector self.minvar = v3 return b_lmn_clip, lamb_clip, frame_clip def get_minvar(self): r"""Access the minimum varience direction vector from mva_gui_class object via this method. """ if self.minvar is not None: out = self.minvar else: raise RuntimeError("Normal has not been calculated yet") return out