4. Machine learning pipeline for detection of boundaries around Mercury

4.1. Installation

Step 1: Download the data from http://epn2024.sinp.msu.ru/epsc2021/ and save it under data/

Step 2: Install the conda environmet : conda env create -f environment.yml

Step 3: Run jupyter notebook Magnetometer.ipynb

4.2. Imports

### standard imports
from __future__ import division
import numpy as np
import pandas as pd
import os, copy
import re
import pickle
import time

### sklearn imports
from sklearn import metrics

### pytorch imports 
import torch
import torch.nn as nn
from torch.nn.parameter import Parameter
import torch.nn.functional as F
import torchvision.transforms as transforms
import torch.utils.data as data
from torch.autograd import Variable
torch.manual_seed(0)

## plotting imports
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.graph_objs as go
from IPython.display import Image


### silence future warnings
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
/var/folders/x3/2bzh843n0tv469w6l6sd8sq00000gn/T/ipykernel_23905/2753096250.py in <module>
     12 
     13 ### pytorch imports
---> 14 import torch
     15 import torch.nn as nn
     16 from torch.nn.parameter import Parameter

ModuleNotFoundError: No module named 'torch'

4.3. Example orbit data

df = pd.read_csv('../../../../labelled_orbits/train/df_10.csv')
df.head()
Unnamed: 0 DATE X_MSO Y_MSO Z_MSO BX_MSO BY_MSO BZ_MSO DBX_MSO DBY_MSO ... Z VX VY VZ VABS D COSALPHA EXTREMA ORBIT LABEL
0 347717 2011-03-28 08:17:09 8216.028 2915.743 -15375.351 27.674 -11.254 12.281 0.136 0.316 ... 6.238218e+06 -36.116236 -39.06727 0.121972 53.203843 5.181665e+07 0.873091 2 10 0
1 347718 2011-03-28 08:17:10 8216.439 2916.030 -15375.077 26.448 -11.211 13.221 0.515 0.338 ... 6.238218e+06 -36.116236 -39.06727 0.121909 53.203843 5.181665e+07 0.873091 0 10 0
2 347719 2011-03-28 08:17:11 8216.850 2916.316 -15374.803 26.505 -11.706 12.536 0.251 1.426 ... 6.238218e+06 -36.116236 -39.06727 0.121926 53.203843 5.181665e+07 0.873091 0 10 0
3 347720 2011-03-28 08:17:12 8217.261 2916.603 -15374.529 25.981 -18.053 6.531 0.379 1.536 ... 6.238218e+06 -36.116236 -39.06727 0.121968 53.203843 5.181665e+07 0.873091 0 10 0
4 347721 2011-03-28 08:17:13 8217.671 2916.889 -15374.255 25.847 -18.826 5.695 0.103 0.301 ... 6.238218e+06 -36.116236 -39.06727 0.121897 53.203843 5.181665e+07 0.873091 0 10 0

5 rows × 32 columns

df.columns
Index(['Unnamed: 0', 'DATE', 'X_MSO', 'Y_MSO', 'Z_MSO', 'BX_MSO', 'BY_MSO',
       'BZ_MSO', 'DBX_MSO', 'DBY_MSO', 'DBZ_MSO', 'RHO_DIPOLE', 'PHI_DIPOLE',
       'THETA_DIPOLE', 'BABS_DIPOLE', 'BX_DIPOLE', 'BY_DIPOLE', 'BZ_DIPOLE',
       'RHO', 'RXY', 'X', 'Y', 'Z', 'VX', 'VY', 'VZ', 'VABS', 'D', 'COSALPHA',
       'EXTREMA', 'ORBIT', 'LABEL'],
      dtype='object')
df[['BX_MSO', 'BY_MSO', 'BZ_MSO']].plot(figsize = (13,8))
<AxesSubplot:>
../../../_images/Magnetometer_8_1.png

4.4. Data Description

  1. X_MSO : Spacecraft position X in MSO coord system [km]

  2. Y_MSO : Spacecraft position Y in MSO coord system [km]

  3. Z_MSO : Spacecraft position Z in MSO coord system [km]

  4. BX_MSO: Magnetic field X component in MSO coord system [nT]

  5. BY_MSO: Magnetic field Y component in MSO coord system [nT]

  6. BZ_MSO: Magnetic field X component in MSO coord system [nT]

  7. DBX_MSO: Magnetic field X component in MSO coord system [nT]

  8. DXY_MSO: Magnetic field Y component in MSO coord system [nT]

  9. DBZ_MSO: Magnetic field X component in MSO coord system [nT]

  10. RHO: Planetocentric spacecraft distance [km]

  11. RXY: Spacecraft distance to the MSO Z axis [km]

  12. RHO_DIPOLE : Dipole-centric spacecraft distance [km]

  13. THETA_DIPOLE: Spacecraft magnetic latitude [rad]

  14. BABS_DIPOLE: Planetary dipole total magnetic field magnitude [nT]

  15. BX_DIPOLE: Planetary dipole magnetic field XMSO component [nT]

  16. BY_DIPOLE: Planetary dipole magnetic field in YMSO component [nT]

  17. BZ_DIPOLE: Planetary dipole magnetic field ZMSO componenet [nT]

  18. X : Mercury Heliocentric position X in SE coordinate system [km]

  19. Y : Mercury Heliocentric position Y in SE coordinate system [km]

  20. Z : Mercury Heliocentric position Z in SE coordinate system [km]

  21. VX : Mercury orbital velocity component in SE coord system [km/sec]

  22. VY : Mercury orbital velocity component in SE coord system [km/sec]

  23. VZ : Mercury orbital velocity component in SE coord system [km/sec]

  24. VABS: Mercury orbital velocity magnitude [km/sec]

  25. D: Mercuty heliocentric distance [km]

  26. COSALPHA: Cosine of Mercury azimuth angle in SE coord system

  27. EXTREMA: 1 in apoapsis, -1 in periapsis, 0 otherwise

### important column names
DATE_COL = "DATE"
ORBIT_COL = "ORBIT"
LABEL_COL = "LABEL"

### relevant feature column names
COLS_3D = [
    ["X", "Y", "Z"],
    ["VX", "VY", "VZ"],
    ["X_MSO", "Y_MSO", "Z_MSO"],
    ["BX_MSO", "BY_MSO", "BZ_MSO"],
    ["DBX_MSO", "DBY_MSO", "DBZ_MSO"],
    ["BX_DIPOLE", "BY_DIPOLE", "BZ_DIPOLE"],
]
COLS_SINGLE = [
    "RHO_DIPOLE",
    "PHI_DIPOLE",
    "THETA_DIPOLE",
    "BABS_DIPOLE",
    "RHO", "RXY",
    "VABS", "D",
    "COSALPHA"
]
FLUX_COLS = COLS_3D[3]

### class name mapping
LABEL_NAMES = {
    0: "interplanetary magnetic field",
    1: "bow shock crossing",
    2: "magnetosheath",
    3: "magnetopause crossing",
    4: "magnetosphere"
}

### event columns in label file
EVENT_COLS = list(range(1, 9))
### Helper functions to normalise data

def normalize_decoupled(data, cols):
    data[cols] = (data[cols] - data[cols].mean()) / data[cols].std()


def normalize_coupled(data, cols):
    centered = data[cols] - data[cols].mean()
    distdev = (centered ** 2).sum(axis=1).mean() ** 0.5
    data[cols] = centered / distdev

4.5. Prepare Dataloader

4.5.1. (a) with sliding window

### Data loader with sliding window

class MessengerDataset(data.Dataset):
    """
    The MESSENGER dataset, cut into fixed-length time windows.
    """

    # TODO add parameters for 3D and 1D feature column names
    def __init__(self, orbits_path, *, window_size=8, normalize=True):
        """
        Loads the MESSENGER dataset from orbits_path and configures it according to the parameters.

        Parameters
        ----------
        orbits_path : str
            The path to the folder containing all MESSENGER orbit data.
        window_size : int, default 8
            The size each window shall have.
        normalize : bool, default True
            Whether to normalize the features.
        """

        self.data = pd.read_csv(orbits_path)
        self.window_size = window_size
        self.skip_size = window_size - 1

        orbit_sizes = self.data.groupby(ORBIT_COL).size()
        self.cum_window_nums = (orbit_sizes - self.skip_size).cumsum()

        if normalize:
            for feat_3d in COLS_3D:
                normalize_coupled(self.data, feat_3d)
            normalize_decoupled(self.data, COLS_SINGLE)

    def __len__(self):
        """
        Yields the size of the dataset.

        Returns
        -------
        int
            The total number of windows.
        """

        return self.cum_window_nums.iloc[-1]

    def __getitem__(self, idx):
        """
        Retrieves the window the given index and its corresponding label.

        Parameters
        ----------
        idx
            The index of the desired window.

        Returns
        -------
        sample : bool
            The requested window as float tensor.
        label : int
            The corresponding label between 0 and 4.

        Raises
        ------
        IndexError
            If idx is negative or larger than the dataset size.
        """

        if idx < 0 or idx >= len(self):
            raise IndexError(f"Index {idx} is out of bounds.")

        orbit_skips = (self.cum_window_nums <= idx).sum()
        pos = idx + orbit_skips * self.skip_size
        window = self.data.iloc[pos:(pos + self.window_size)]

        feats_3d = [torch.tensor(window[feat].values) for feat in COLS_3D]
        sample = torch.cat(feats_3d).view(self.window_size, -1, 3)  # window_size x #feats_3d x 3
        label = torch.tensor(window[LABEL_COL].values)  # list of ordinal labels

        return sample, label
dataset = Magnetometer_data('../../../../labelled_orbits/train/df_10.csv', features)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-57-cfdba41dad21> in <module>
----> 1 dataset = Magnetometer_data('../../../../labelled_orbits/train/df_10.csv', features)

NameError: name 'features' is not defined
len(dataset)
43466

4.5.2. (b) without sliding window

### Data loader without windowing

class Magnetometer_data(data.Dataset):
    def __init__(self,  csv_file , features, sample_rate = 1, transform = None, root = ''):
        """Args:
           root (string): Base path
           features (list): selected features
           csv_file (string): Path to csv file with sensor data
           transform (callable, optional): Transforms to be applied.
        """
        assert len(features) % 3 == 0
        self.data_path = os.path.join(root, csv_file)
        self.feature_cols = features
        self.sample_rate = sample_rate
        self.data = pd.read_csv(self.data_path, usecols = self.feature_cols).dropna()
        # self.data = self.__normalise__(self.data)
        self.transform = transform
        try:
            self.labels = pd.read_csv(self.data_path, usecols = ['LABEL'])
        except ValueError as e:
            self.labels = None
    
    def __len__(self):
        return len(self.data)
    
    def __label_to_int(self, label):
        label_ord = {'IMF': 0, 'BS-crossing': 1, 'MP-crossing' : 3, 'magnetosheath': 2, 'magnetosphere': 4}
        return label_ord[label]
    
    def __normalise__(self, features):
        mean = np.mean(features)
        std = np.std(features)
        return (features - mean)/std
    
    def __reshape__(self, x):
        ##reshape bands, features
        return x.reshape(-1,3)
        #return torch.unsqueeze(x,0)
    
    def __getitem__(self, idx):
       
        coords = self.__reshape__(torch.tensor(self.data.iloc[idx].values))
        if self.transform:
            coords = self.transform(coords)
        
        if self.labels is not None:
            #label_int = torch.tensor(self.__label_to_int(self.labels.iloc[idx]['labels']))
            label_int = self.labels.iloc[idx]['LABEL']
            sample = {'coords': coords, 'labels': label_int}
            return sample
        else:
            sample = {'coords': coords}
            return sample

4.6. Convolutions

The convolution layer uses filters that perform convolution operations as it is scanning the input \(I\) with respect to its dimensions. Its hyperparameters include the filter size \(F\) and stride \(S\). The resulting output \(O\) is called feature map or activation map.

Image('img/convolution-layer-a.png')
../../../_images/Magnetometer_20_0.png
class CNN(nn.Module):
    def __init__(self, input_dim = 3, bands = 1):
        """
        Description: 
        3 layer simple CNN followed by 2 FC layers. 
        Args:
        input_dim: dimensions in the data, default = 3 for X, Y,Z
        bands : number of features/channels. eg: Aircraft position --> 1
        """
        super(CNN, self).__init__()
        self.bands = bands
        self.conv1 = nn.Conv1d(input_dim, 32, 1)
        self.conv2 = nn.Conv1d(32, 64, 1)
        self.conv3 = nn.Conv1d(64, 128, 1)
        self.fc1 = nn.Linear(128 * self.bands , 64)
        self.fc2 = nn.Linear(64, 5)
        self.activation = nn.LogSoftmax(dim = 1)
        
    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = F.relu(self.conv3(x))
        x = x.reshape(-1, 128 * self.bands)
        x = F.relu(self.fc1(x))
        x = self.activation(self.fc2(x))
      
        return x
Image('img/cnn_magnetometer.png')
../../../_images/Magnetometer_22_0.png
### Simple n layer LSTM network, modified from nn.LSTM
class LSTM(nn.Module):
    def __init__(self, input_dim, hidden_dim, layer_dim, output_dim):
        """
        Description: 
        Simple LSTM with recurrence 
        Args:
        input_dim: dimensions in the data, default
        layer_dim: number of layers
        output_dim: output units
        """
        super(LSTM, self).__init__()
        # Hidden dimensions
        self.hidden_dim = hidden_dim

        # Number of hidden layers
        self.layer_dim = layer_dim

        # batch_first=True causes input/output tensors to be of shape
        # (batch_dim, seq_dim, feature_dim)
        self.lstm = nn.LSTM(input_dim, hidden_dim, layer_dim, batch_first=True)

        # Readout layer
        self.fc = nn.Linear(hidden_dim*2, output_dim)

    def forward(self, x):
        b = x.size(0)
        # Initialize hidden state with zeros
        h0 = torch.zeros(self.layer_dim, x.size(0), self.hidden_dim, device = device,).requires_grad_()

        # Initialize cell state
        c0 = torch.zeros(self.layer_dim, x.size(0), self.hidden_dim, device = device).requires_grad_()

        # We need to detach for backpropagation through time (BPTT)
        # If we don't, we'll backprop all the way to the start even after going through another batch
        out, (hn, cn) = self.lstm(x, (h0.detach(), c0.detach()))
        # Index hidden state of last time step
        out = self.fc(torch.reshape(out,(b,-1)))
        return out
class CRNN(nn.Module):
    def __init__(self, input_dim = 16, hidden_dim = 16, n_layers = 2,bands = 1):
        """ 
        Args:
        input_dim: dimensions in the data, default = 3 for X, Y,Z
        bands : number of features/channels. eg: Aircraft position --> 1
        """
        super(CRNN, self).__init__()
        self.bands = bands
        self.hidden_dim = hidden_dim
        self.n_layers = n_layers
        self.cnn1 = nn.Conv1d(input_dim, 32,1)
        self.cnn2 = nn.Conv1d(32, 64, 1 )
        self.bn1 = nn.BatchNorm1d(32)
        self.bn2 = nn.BatchNorm1d(64)
        self.fc0 = nn.Linear(hidden_dim, 8)
        self.fc1 = nn.Linear(8 , 5)
        #self.fc4 = nn.Linear(64, 5)
        self.activation = nn.LogSoftmax(dim = -1)
        self.rnn = nn.RNN(64, self.hidden_dim, self.n_layers, batch_first=True)   


        
    def init_hidden(self, batch_size):
        # This method generates the first hidden state of zeros which we'll use in the forward pass
        # We'll send the tensor holding the hidden state to the device we specified earlier as well
        hidden = torch.zeros(self.n_layers, batch_size, self.hidden_dim).to(device)
        return hidden
    
    
    
    def forward(self, x):
        b = x.size(0)
        t = x.size(2)
        #h,w = x.size(2), x.size(3)
        x = F.relu(self.bn1(self.cnn1(x)))
        x = F.relu(self.bn2(self.cnn2(x)))
        x = x.transpose(1,2)
        # Initializing hidden state for first input using method defined below
        hidden = self.init_hidden(b)
        # Passing in the input and hidden state into the model and obtaining outputs
        out, hidden = self.rnn(x, hidden)
        hidden = hidden.permute(0,2,1)
        #hidden = hidden.view(b,t,-1)
        out = out.contiguous().view(b,t,-1)
        x = F.relu((self.fc0(out)))
        x = F.relu((self.fc1(x)))
        x = self.activation(x)
        #x = x.reshape(b, t, 5)
        return x
    
class CNN_window(nn.Module):
    def __init__(self, input_dim = 3, bands = 1):
        """ 
        Args:
        input_dim: dimensions in the data, default = 3 for X, Y,Z
        bands : number of features/channels. eg: Aircraft position --> 1
        """
        super(CNN_window, self).__init__()
        self.bands = bands
        self.conv1 = nn.Conv1d(input_dim, 32, 1)
        self.bn1 = nn.BatchNorm1d(32)
        self.conv2 = nn.Conv1d(32, 64, 1)
        self.bn2 = nn.BatchNorm1d(64)
        self.conv3 = nn.Conv1d(64, 128, 1)
        self.bn3 = nn.BatchNorm1d(128)
        self.fc0 = nn.Linear(128,64)
        self.fc1 = nn.Linear(1, 8)
        self.fc2 = nn.Linear(64 , 32)
        self.fc3 = nn.Linear(32, 5)
        #self.fc4 = nn.Linear(64, 5)
        self.activation = nn.Softmax(dim = -1)
    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.relu(self.bn3(self.conv3(x)))
        x = x.transpose(1,2)
        x = F.relu(self.fc0(x))
        activations = F.relu(self.fc2(x))
        output = self.activation(self.fc3(activations))
        return output
def train(model, optimizer , train_loader, val_loader,criterion = nn.CrossEntropyLoss(),save = False, epochs = 1):
    # iteration_loss = []
    for epoch in range(epochs):
        model.train()
        running_loss = 0.0
        for i,sample in enumerate(train_loader,0):
            ## load inputs and labels
            inputs = sample[0].float().to(device)
            inputs = inputs.squeeze(1)
            b = inputs.size(0)
            t = inputs.size(1)
            
            inputs = inputs.reshape(b, t, -1)
            labels = sample[1].long().to(device)

            labels = labels.squeeze(1)
            # labels = torch.nn.functional.one_hot(labels_int)

            ## set grad to 0
            optimizer.zero_grad()
            
            ## forward propagate
            output = model(inputs.transpose(1,2))
            ## Calculate classification output
            _, predicted = torch.max(output, -1)
           
            loss = criterion(output.view(-1,5), labels.view(-1))
            #loss = criterion(output, labels)
            
            ## backward + optimize only if in training phase
            loss.backward()
            
            ## update the weights
            optimizer.step()
            
            ## print statistics
            running_loss += loss.item()
            if i % 10 == 9:    # print every 10 mini-batches
                    print('[Epoch: %d, Batch: %4d / %4d], loss: %.3f' %
                        (epoch + 1, i + 1, len(train_loader), running_loss / 10))
                    running_loss = 0.0
            # iteration_loss.append(running_loss)
        
        
        ## validation
        if val_loader:
            model.eval()
            correct = total = 0
            with torch.no_grad():
                for sample in val_loader:
                    inputs = sample[0].float().to(device)
                    inputs = inputs.squeeze(1)
                    b = inputs.size(0)
                    t = inputs.size(1)

                    inputs = inputs.reshape(b, t, -1)

                    labels = sample[1].long().to(device)
                    labels = labels.squeeze(1)

                    output = model(inputs.transpose(1,2))
                    _, predicted = torch.max(output, -1)
                    total += labels.size(0)
                    correct += (predicted == labels).sum().item()
            val_acc = 100. * correct / total
            print('Valid accuracy: %d %%' % val_acc)
                    
        ## save
        if save:
            torch.save(model.state_dict(), "save_"+str(epoch)+".pth")
# Load device
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 
# Parameters
params = {'batch_size': 4096,
          'shuffle': True,
          'num_workers': 32}



csv = 'data/sampled_df/df_train.csv'
csv_test = 'data/sampled_df/df_test.csv'
xdata = MessengerDataset(csv, window_size = 1)
test_data = MessengerDataset(csv_test, window_size = 1)
# Train, validation split
# 80/20 split
#train_set, val_set = xdata[: 80], xdata[int(len(xdata)*0.8):]
#train_set, val_set = torch.utils.data.random_split(xdata, [int(len(xdata)*0.8), int(len(xdata)*0.2)])
    
### Create dataloader

train_loader = torch.utils.data.DataLoader(xdata, **params)
validation_loader = torch.utils.data.DataLoader(test_data, **params)
    
### Load model
model = CNN(3,6).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr = 0.0001)
train(model, optimizer, train_loader, validation_loader, epochs = 150, save = False)
[Epoch: 1, Batch:   10 / 3849], loss: 1.589
[Epoch: 1, Batch:   20 / 3849], loss: 1.540
[Epoch: 1, Batch:   30 / 3849], loss: 1.484
[Epoch: 1, Batch:   40 / 3849], loss: 1.419
[Epoch: 1, Batch:   50 / 3849], loss: 1.344
[Epoch: 1, Batch:   60 / 3849], loss: 1.263
[Epoch: 1, Batch:   70 / 3849], loss: 1.175
[Epoch: 1, Batch:   80 / 3849], loss: 1.096
[Epoch: 1, Batch:   90 / 3849], loss: 1.020
[Epoch: 1, Batch:  100 / 3849], loss: 0.963
[Epoch: 1, Batch:  110 / 3849], loss: 0.922
[Epoch: 1, Batch:  120 / 3849], loss: 0.887
[Epoch: 1, Batch:  130 / 3849], loss: 0.860
[Epoch: 1, Batch:  140 / 3849], loss: 0.824
[Epoch: 1, Batch:  150 / 3849], loss: 0.795
[Epoch: 1, Batch:  160 / 3849], loss: 0.761
[Epoch: 1, Batch:  170 / 3849], loss: 0.742
[Epoch: 1, Batch:  180 / 3849], loss: 0.709
[Epoch: 1, Batch:  190 / 3849], loss: 0.677
[Epoch: 1, Batch:  200 / 3849], loss: 0.650
[Epoch: 1, Batch:  210 / 3849], loss: 0.622
[Epoch: 1, Batch:  220 / 3849], loss: 0.593
[Epoch: 1, Batch:  230 / 3849], loss: 0.562
[Epoch: 1, Batch:  240 / 3849], loss: 0.537
[Epoch: 1, Batch:  250 / 3849], loss: 0.509
[Epoch: 1, Batch:  260 / 3849], loss: 0.496
[Epoch: 1, Batch:  270 / 3849], loss: 0.468
[Epoch: 1, Batch:  280 / 3849], loss: 0.462
[Epoch: 1, Batch:  290 / 3849], loss: 0.435
[Epoch: 1, Batch:  300 / 3849], loss: 0.424
[Epoch: 1, Batch:  310 / 3849], loss: 0.409
[Epoch: 1, Batch:  320 / 3849], loss: 0.394
[Epoch: 1, Batch:  330 / 3849], loss: 0.382
[Epoch: 1, Batch:  340 / 3849], loss: 0.378
[Epoch: 1, Batch:  350 / 3849], loss: 0.366
[Epoch: 1, Batch:  360 / 3849], loss: 0.352
[Epoch: 1, Batch:  370 / 3849], loss: 0.339
[Epoch: 1, Batch:  380 / 3849], loss: 0.337
[Epoch: 1, Batch:  390 / 3849], loss: 0.322
[Epoch: 1, Batch:  400 / 3849], loss: 0.323
[Epoch: 1, Batch:  410 / 3849], loss: 0.313
[Epoch: 1, Batch:  420 / 3849], loss: 0.311
[Epoch: 1, Batch:  430 / 3849], loss: 0.305
[Epoch: 1, Batch:  440 / 3849], loss: 0.297
[Epoch: 1, Batch:  450 / 3849], loss: 0.296
[Epoch: 1, Batch:  460 / 3849], loss: 0.287
[Epoch: 1, Batch:  470 / 3849], loss: 0.278
[Epoch: 1, Batch:  480 / 3849], loss: 0.283
[Epoch: 1, Batch:  490 / 3849], loss: 0.278
[Epoch: 1, Batch:  500 / 3849], loss: 0.274
[Epoch: 1, Batch:  510 / 3849], loss: 0.267
[Epoch: 1, Batch:  520 / 3849], loss: 0.265
[Epoch: 1, Batch:  530 / 3849], loss: 0.263
[Epoch: 1, Batch:  540 / 3849], loss: 0.258
[Epoch: 1, Batch:  550 / 3849], loss: 0.256
[Epoch: 1, Batch:  560 / 3849], loss: 0.258
[Epoch: 1, Batch:  570 / 3849], loss: 0.255
[Epoch: 1, Batch:  580 / 3849], loss: 0.248
[Epoch: 1, Batch:  590 / 3849], loss: 0.251
[Epoch: 1, Batch:  600 / 3849], loss: 0.245
[Epoch: 1, Batch:  610 / 3849], loss: 0.247
[Epoch: 1, Batch:  620 / 3849], loss: 0.246
[Epoch: 1, Batch:  630 / 3849], loss: 0.238
[Epoch: 1, Batch:  640 / 3849], loss: 0.244
[Epoch: 1, Batch:  650 / 3849], loss: 0.232
[Epoch: 1, Batch:  660 / 3849], loss: 0.238
[Epoch: 1, Batch:  670 / 3849], loss: 0.238
[Epoch: 1, Batch:  680 / 3849], loss: 0.232
[Epoch: 1, Batch:  690 / 3849], loss: 0.239
[Epoch: 1, Batch:  700 / 3849], loss: 0.230
[Epoch: 1, Batch:  710 / 3849], loss: 0.232
[Epoch: 1, Batch:  720 / 3849], loss: 0.234
[Epoch: 1, Batch:  730 / 3849], loss: 0.228
[Epoch: 1, Batch:  740 / 3849], loss: 0.226
[Epoch: 1, Batch:  750 / 3849], loss: 0.227
[Epoch: 1, Batch:  760 / 3849], loss: 0.229
[Epoch: 1, Batch:  770 / 3849], loss: 0.221
[Epoch: 1, Batch:  780 / 3849], loss: 0.219
[Epoch: 1, Batch:  790 / 3849], loss: 0.219
[Epoch: 1, Batch:  800 / 3849], loss: 0.222
[Epoch: 1, Batch:  810 / 3849], loss: 0.221
[Epoch: 1, Batch:  820 / 3849], loss: 0.217
[Epoch: 1, Batch:  830 / 3849], loss: 0.215
[Epoch: 1, Batch:  840 / 3849], loss: 0.223
[Epoch: 1, Batch:  850 / 3849], loss: 0.215
[Epoch: 1, Batch:  860 / 3849], loss: 0.219
[Epoch: 1, Batch:  870 / 3849], loss: 0.218
[Epoch: 1, Batch:  880 / 3849], loss: 0.215
[Epoch: 1, Batch:  890 / 3849], loss: 0.214
[Epoch: 1, Batch:  900 / 3849], loss: 0.218
[Epoch: 1, Batch:  910 / 3849], loss: 0.210
[Epoch: 1, Batch:  920 / 3849], loss: 0.213
[Epoch: 1, Batch:  930 / 3849], loss: 0.206
[Epoch: 1, Batch:  940 / 3849], loss: 0.213
[Epoch: 1, Batch:  950 / 3849], loss: 0.209
[Epoch: 1, Batch:  960 / 3849], loss: 0.208
[Epoch: 1, Batch:  970 / 3849], loss: 0.206
[Epoch: 1, Batch:  980 / 3849], loss: 0.205
[Epoch: 1, Batch:  990 / 3849], loss: 0.204
[Epoch: 1, Batch: 1000 / 3849], loss: 0.208
[Epoch: 1, Batch: 1010 / 3849], loss: 0.205
[Epoch: 1, Batch: 1020 / 3849], loss: 0.203
[Epoch: 1, Batch: 1030 / 3849], loss: 0.200
[Epoch: 1, Batch: 1040 / 3849], loss: 0.200
[Epoch: 1, Batch: 1050 / 3849], loss: 0.204
[Epoch: 1, Batch: 1060 / 3849], loss: 0.202
[Epoch: 1, Batch: 1070 / 3849], loss: 0.198
[Epoch: 1, Batch: 1080 / 3849], loss: 0.202
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-58-e70daca3e090> in <module>
      2 model = CNN(3,6).to(device)
      3 optimizer = torch.optim.Adam(model.parameters(), lr = 0.0001)
----> 4 train(model, optimizer, train_loader, validation_loader, epochs = 150, save = False)

<ipython-input-55-272714bc5674> in train(model, optimizer, train_loader, val_loader, criterion, save, epochs)
      4         model.train()
      5         running_loss = 0.0
----> 6         for i,sample in enumerate(train_loader,0):
      7             ## load inputs and labels
      8             inputs = sample[0].float().to(device)

~/anaconda3/lib/python3.8/site-packages/torch/utils/data/dataloader.py in __next__(self)
    515             if self._sampler_iter is None:
    516                 self._reset()
--> 517             data = self._next_data()
    518             self._num_yielded += 1
    519             if self._dataset_kind == _DatasetKind.Iterable and \

~/anaconda3/lib/python3.8/site-packages/torch/utils/data/dataloader.py in _next_data(self)
   1180 
   1181             assert not self._shutdown and self._tasks_outstanding > 0
-> 1182             idx, data = self._get_data()
   1183             self._tasks_outstanding -= 1
   1184             if self._dataset_kind == _DatasetKind.Iterable:

~/anaconda3/lib/python3.8/site-packages/torch/utils/data/dataloader.py in _get_data(self)
   1146         else:
   1147             while True:
-> 1148                 success, data = self._try_get_data()
   1149                 if success:
   1150                     return data

~/anaconda3/lib/python3.8/site-packages/torch/utils/data/dataloader.py in _try_get_data(self, timeout)
    984         #   (bool: whether successfully get data, any: data if successful else None)
    985         try:
--> 986             data = self._data_queue.get(timeout=timeout)
    987             return (True, data)
    988         except Exception as e:

~/anaconda3/lib/python3.8/multiprocessing/queues.py in get(self, block, timeout)
    105                 if block:
    106                     timeout = deadline - time.monotonic()
--> 107                     if not self._poll(timeout):
    108                         raise Empty
    109                 elif not self._poll():

~/anaconda3/lib/python3.8/multiprocessing/connection.py in poll(self, timeout)
    255         self._check_closed()
    256         self._check_readable()
--> 257         return self._poll(timeout)
    258 
    259     def __enter__(self):

~/anaconda3/lib/python3.8/multiprocessing/connection.py in _poll(self, timeout)
    422 
    423     def _poll(self, timeout):
--> 424         r = wait([self], timeout)
    425         return bool(r)
    426 

~/anaconda3/lib/python3.8/multiprocessing/connection.py in wait(object_list, timeout)
    929 
    930             while True:
--> 931                 ready = selector.select(timeout)
    932                 if ready:
    933                     return [key.fileobj for (key, events) in ready]

~/anaconda3/lib/python3.8/selectors.py in select(self, timeout)
    413         ready = []
    414         try:
--> 415             fd_event_list = self._selector.poll(timeout)
    416         except InterruptedError:
    417             return ready

KeyboardInterrupt: 

4.7. Evaluation Script

with torch.no_grad():
    model.eval()
    correct = total = 0
    with torch.no_grad():
        for sample in validation_loader:
            inputs = sample[0].float().to(device)
            inputs = inputs.squeeze(1)
            b = inputs.size(0)
            t = inputs.size(1)

            inputs = inputs.reshape(b, t, -1)

            labels = sample[1].long().to(device)
            labels = labels.squeeze(1)

            output = model(inputs.transpose(1,2))
            _, predicted = torch.max(output, -1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
            
                    
        val_acc = 100. * correct / total
        print('Valid accuracy: %f %%' % val_acc)
def plot_cm(model, validation_loader):
    total = correct = 0
    conf_mat = []
    with torch.no_grad():
        for i,sample in enumerate(validation_loader):
            if i % 1 == 0:
                inputs = sample[0].float().to(device)
                inputs = inputs.squeeze()
                inputs = inputs.squeeze(1)
                b = inputs.size(0)
                t = inputs.size(1)
                inputs = inputs.reshape(b,t,-1)
                labels = sample[1].long().to(device)
                output = model(inputs.transpose(1,2))
                _, predicted = torch.max(output, -1)
                #print(torch.unique(labels))
                #print(torch.unique(predicted))
                conf_matrix = metrics.confusion_matrix(predicted.float().cpu(), labels.float().cpu())
                conf_mat.append(conf_matrix)
                total += labels.size(0)
                correct += (predicted.ravel() == labels.ravel()).sum().item()
    agg_cm = np.sum(conf_mat,0)
    agg_cm = agg_cm/np.sum(agg_cm,0)
    classes = np.array(['IMF','BS-crossing','magnetosheath','MP-crossing','magnetosphere'])
    con_mat_df = pd.DataFrame(
        agg_cm, index = classes,columns = classes)
    ##plot
    figure = plt.figure(figsize=(8, 8))
    sns.heatmap(con_mat_df, annot=True,cmap=plt.cm.Blues)
    return con_mat_df
plot_cm(model, validation_loader)
IMF BS-crossing magnetosheath MP-crossing magnetosphere
IMF 9.845936e-01 0.358994 0.077034 0.006411 0.000386
BS-crossing 1.439123e-02 0.528224 0.158833 0.008233 0.000007
magnetosheath 1.014515e-03 0.112715 0.705087 0.357031 0.003596
MP-crossing 6.703105e-07 0.000055 0.033648 0.443121 0.020106
magnetosphere 0.000000e+00 0.000011 0.025398 0.185204 0.975905
../../../_images/Magnetometer_33_1.png
def plot_field(df):
    fig = go.Figure()
    
    # Plotting components of the magnetic field B_x, B_y, B_z in MSO coordinates
    fig.add_trace(go.Scatter(x=df['DATE'], y=df['BX_MSO'], name='B_x'))
    fig.add_trace(go.Scatter(x=df['DATE'], y=df['BY_MSO'], name='B_y'))
    fig.add_trace(go.Scatter(x=df['DATE'], y=df['BZ_MSO'], name='B_z'))

    # Plotting total magnetic field magnitude B along the orbit
    fig.add_trace(go.Scatter(x=df['DATE'], y=-df['B_tot'], name='|B|', line_color = 'darkgray'))
    fig.add_trace(go.Scatter(x=df['DATE'], y=df['B_tot'], name='|B|', line_color = 'darkgray', showlegend=False))
    return fig
def show_plot(df, model, annotation, orbit_no, evaluate = False):
    fig = plot_field(df)
    df['Label'] = annotation
    bowshock = df.loc[df['Label'] == 1]
    magnetopause = df.loc[df['Label'] == 3]


    for i in range(len(bowshock['DATE'])):
        fig.add_trace(go.Scatter(x=[bowshock['DATE'].iloc[i], bowshock['DATE'].iloc[i]],
                                     y=[-450, 450],
                                     mode='lines',
                                     name='Bow Shock',
                                     line_color='black',
                                     showlegend=False
            ))

    for i in range(len(magnetopause['DATE'])):
            fig.add_trace(go.Scatter(x=[magnetopause['DATE'].iloc[i], magnetopause['DATE'].iloc[i]],
                                     y=[-450, 450],
                                     mode='lines',
                                     name='Magnetopause',
                                     line_color = 'purple',
                                     showlegend=False
            ))
    print(orbit_no)

    if evaluate == True:
        fig.update_layout({'title': f'Messenger orbit {orbit_no:04d} [Learned]'})
    else:
        fig.update_layout({'title': f'Messgener orbit {orbit_no:04d} [Ground Truth]'})

    fig.show()
model = CNN(3,6).to(device)
model.load_state_dict(torch.load('model-windowed-cnn.pth'))
<All keys matched successfully>
model_partial = CNN(3,4).to(device)
model_partial.load_state_dict(torch.load('saved_models/model-classification-partial.pth'))

features_list = ['X_MSO', 'Y_MSO', 'Z_MSO','BX_MSO', 'BY_MSO', 'BZ_MSO', 'VX', 'VY', 'VZ', 'EXTREMA', 'COSALPHA', 'RHO_DIPOLE']
def plot_prediction(model,orbit_no, features_list):
    orbit_no = int(orbit_no)
    total = correct = 0
    csv = f'../03-notebooks/labelled_orbits/train/test/df_{orbit_no:d}.csv'
    df = pd.read_csv(csv, sep = ',')
    label = df.LABEL
    df["B_tot"] = (df["BX_MSO"]**2 + df["BY_MSO"]**2 + df["BZ_MSO"]**2)**0.5
    orbit = Magnetometer_data(csv, features_list)
    annotation = []
    for sample in orbit:
        coords = torch.tensor(sample['coords']).float().to(device)
        coords = torch.unsqueeze(coords,0)
        output = model_partial(coords.transpose(1,2))
        predicted = output.argmax(-1)
        annotation.append(predicted)



    show_plot(df, model_partial, annotation, orbit_no, evaluate = True)
    show_plot(df, model_partial, label, orbit_no)
plot_prediction(model_partial, 1790, features_list)
<ipython-input-18-edbe06637cdc>:11: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).
  coords = torch.tensor(sample['coords']).float().to(device)
1790
1790
plot_prediction(model_partial, 94, features_list)
<ipython-input-18-edbe06637cdc>:11: UserWarning:

To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).
94
94