diff --git a/docs/source/_static/elfin_epd.png b/docs/source/_static/elfin_epd.png new file mode 100644 index 00000000..f3f82c63 Binary files /dev/null and b/docs/source/_static/elfin_epd.png differ diff --git a/pyspedas/__init__.py b/pyspedas/__init__.py index 3360b1c9..2546b4bf 100644 --- a/pyspedas/__init__.py +++ b/pyspedas/__init__.py @@ -75,6 +75,7 @@ from . import akebono from . import soho from . import barrel +from . import elfin # set up logging/console output import logging diff --git a/pyspedas/elfin/README.md b/pyspedas/elfin/README.md new file mode 100644 index 00000000..c5dcb1e9 --- /dev/null +++ b/pyspedas/elfin/README.md @@ -0,0 +1,75 @@ + +## Electron Losses and Fields Investigation (ELFIN) +The routines in this module can be used to load data from the Electron Losses and Fields Investigation (ELFIN) mission. + +### Instruments +- Fluxgate Magnetometer (FGM) +- Energetic Particle Detector (EPD) +- Magneto Resistive Magnetometer-a (MRMa) +- Magneto Resistive Magnetometer-i (MRMi) +- State data (state) +- Engineering data (ENG) + +### Examples +Get started by importing pyspedas and tplot; these are required to load and plot the data: + +```python +import pyspedas +from pytplot import tplot +``` + +#### Fluxgate Magnetometer (FGM) + +```python +fgm_vars = pyspedas.elfin.fgm(trange=['2020-10-01', '2020-10-02']) + +tplot('ela_fgs') +``` + + +#### Energetic Particle Detector (EPD) + +```python +epd_vars = pyspedas.elfin.epd(trange=['2020-11-01', '2020-11-02']) + +tplot('ela_pef') +``` + + +#### Magneto Resistive Magnetometer (MRMa) + +```python +mrma_vars = pyspedas.elfin.mrma(trange=['2020-11-5', '2020-11-6']) + +tplot('ela_mrma') +``` + + +#### Magneto Resistive Magnetometer (MRMi) + +```python +mrmi_vars = pyspedas.elfin.mrmi(trange=['2020-11-5', '2020-11-6']) + +tplot('ela_mrmi') +``` + + +#### State data (state) + +```python +state_vars = pyspedas.elfin.state(trange=['2020-11-5/10:00', '2020-11-5/12:00']) + +tplot('ela_pos_gei') +``` + + +#### Engineering (ENG) + +```python +eng_vars = pyspedas.elfin.eng(trange=['2020-11-5', '2020-11-6']) + +tplot('ela_fc_idpu_temp') +``` + + + \ No newline at end of file diff --git a/pyspedas/elfin/__init__.py b/pyspedas/elfin/__init__.py new file mode 100644 index 00000000..650e5b47 --- /dev/null +++ b/pyspedas/elfin/__init__.py @@ -0,0 +1,427 @@ +from functools import wraps + +from .load import load +from .epd.epd import elfin_load_epd + +@wraps(elfin_load_epd) +def epd(*args, **kwargs): + return elfin_load_epd(*args, **kwargs) + +def fgm(trange=['2020-10-01', '2020-10-02'], + probe='a', + datatype='survey', + level='l1', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=False): + """ + This function loads data from the Fluxgate Magnetometer (FGM) + + Parameters + ---------- + trange : list of str + time range of interest [starttime, endtime] with the format + 'YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + probe: str + Spacecraft identifier ('a' or 'b') + + datatype: str + Data type; Valid options: + 'fast', 'survey' for L1 data + + level: str + Data level; options: 'l1' (default: l1) + + suffix: str + The tplot variable names will be given this suffix. By default, + no suffix is added. + + get_support_data: bool + Data with an attribute "VAR_TYPE" with a value of "support_data" + will be loaded into tplot. By default, only loads in data with a + "VAR_TYPE" attribute of "data". + + varformat: str + The file variable formats to load into tplot. Wildcard character + "*" is accepted. By default, all variables are loaded in. + + varnames: list of str + List of variable names to load (if not specified, + all data variables are loaded) + + downloadonly: bool + Set this flag to download the CDF files, but not load them into + tplot variables + + notplot: bool + Return the data in hash tables instead of creating tplot variables + + no_update: bool + If set, only load data from your local cache + + time_clip: bool + Time clip the variables to exactly the range specified in the trange keyword + + Returns + ---------- + List of tplot variables created. + + """ + tvars = load(instrument='fgm', probe=probe, trange=trange, level=level, + datatype=datatype, suffix=suffix, get_support_data=get_support_data, + varformat=varformat, varnames=varnames, downloadonly=downloadonly, + notplot=notplot, time_clip=time_clip, no_update=no_update) + + if tvars is None or notplot or downloadonly: + return tvars + + return fgm_postprocessing(tvars) + + +def fgm_postprocessing(variables): + """ + Placeholder for FGM post-processing + """ + return variables + + + + + +def mrma(trange=['2020-11-5', '2020-11-6'], + probe='a', + datatype='mrma', + level='l1', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=False): + """ + This function loads data from the Magneto Resistive Magnetometer (MRMa) + + Parameters + ---------- + trange : list of str + time range of interest [starttime, endtime] with the format + 'YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + probe: str + Spacecraft identifier ('a' or 'b') + + datatype: str + Data type; Valid options: + 'mrma' for L1 data + + level: str + Data level; options: 'l1' (default: l1) + + suffix: str + The tplot variable names will be given this suffix. By default, + no suffix is added. + + get_support_data: bool + Data with an attribute "VAR_TYPE" with a value of "support_data" + will be loaded into tplot. By default, only loads in data with a + "VAR_TYPE" attribute of "data". + + varformat: str + The file variable formats to load into tplot. Wildcard character + "*" is accepted. By default, all variables are loaded in. + + varnames: list of str + List of variable names to load (if not specified, + all data variables are loaded) + + downloadonly: bool + Set this flag to download the CDF files, but not load them into + tplot variables + + notplot: bool + Return the data in hash tables instead of creating tplot variables + + no_update: bool + If set, only load data from your local cache + + time_clip: bool + Time clip the variables to exactly the range specified in the trange keyword + + Returns + ---------- + List of tplot variables created. + + """ + tvars = load(instrument='mrma', probe=probe, trange=trange, level=level, datatype=datatype, suffix=suffix, get_support_data=get_support_data, varformat=varformat, varnames=varnames, downloadonly=downloadonly, notplot=notplot, time_clip=time_clip, no_update=no_update) + + if tvars is None or notplot or downloadonly: + return tvars + + return mrma_postprocessing(tvars) + + +def mrma_postprocessing(variables): + """ + Placeholder for MRMa post-processing + """ + return variables + + +def mrmi(trange=['2020-11-5', '2020-11-6'], + probe='a', + datatype='mrmi', + level='l1', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=False): + """ + This function loads data from the Magneto Resistive Magnetometer (MRMi) + + Parameters + ---------- + trange : list of str + time range of interest [starttime, endtime] with the format + 'YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + probe: str + Spacecraft identifier ('a' or 'b') + + datatype: str + Data type; Valid options: + 'mrmi' for L1 data + + level: str + Data level; options: 'l1' (default: l1) + + suffix: str + The tplot variable names will be given this suffix. By default, + no suffix is added. + + get_support_data: bool + Data with an attribute "VAR_TYPE" with a value of "support_data" + will be loaded into tplot. By default, only loads in data with a + "VAR_TYPE" attribute of "data". + + varformat: str + The file variable formats to load into tplot. Wildcard character + "*" is accepted. By default, all variables are loaded in. + + varnames: list of str + List of variable names to load (if not specified, + all data variables are loaded) + + downloadonly: bool + Set this flag to download the CDF files, but not load them into + tplot variables + + notplot: bool + Return the data in hash tables instead of creating tplot variables + + no_update: bool + If set, only load data from your local cache + + time_clip: bool + Time clip the variables to exactly the range specified in the trange keyword + + Returns + ---------- + List of tplot variables created. + + """ + tvars = load(instrument='mrmi', probe=probe, trange=trange, level=level, datatype=datatype, suffix=suffix, get_support_data=get_support_data, varformat=varformat, varnames=varnames, downloadonly=downloadonly, notplot=notplot, time_clip=time_clip, no_update=no_update) + + if tvars is None or notplot or downloadonly: + return tvars + + return mrmi_postprocessing(tvars) + + +def mrmi_postprocessing(variables): + """ + Placeholder for MRMi post-processing + """ + return variables + + +def state(trange=['2020-11-5/10:00', '2020-11-5/12:00'], + probe='a', + datatype='defn', + level='l1', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=True): + """ + This function loads data from the State data (state) + + Parameters + ---------- + trange : list of str + time range of interest [starttime, endtime] with the format + 'YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + probe: str + Spacecraft identifier ('a' or 'b') + + datatype: str + Data type; Valid options: + 'defn' (default) + 'pred' + + level: str + Data level; options: 'l1' (default: l1) + + suffix: str + The tplot variable names will be given this suffix. By default, + no suffix is added. + + get_support_data: bool + Data with an attribute "VAR_TYPE" with a value of "support_data" + will be loaded into tplot. By default, only loads in data with a + "VAR_TYPE" attribute of "data". + + varformat: str + The file variable formats to load into tplot. Wildcard character + "*" is accepted. By default, all variables are loaded in. + + varnames: list of str + List of variable names to load (if not specified, + all data variables are loaded) + + downloadonly: bool + Set this flag to download the CDF files, but not load them into + tplot variables + + notplot: bool + Return the data in hash tables instead of creating tplot variables + + no_update: bool + If set, only load data from your local cache + + time_clip: bool + Time clip the variables to exactly the range specified in the trange keyword + + Returns + ---------- + List of tplot variables created. + + """ + tvars= load(instrument='state', probe=probe, trange=trange, level=level, datatype=datatype, suffix=suffix, get_support_data=get_support_data, varformat=varformat, varnames=varnames, downloadonly=downloadonly, notplot=notplot, time_clip=time_clip, no_update=no_update) + + if tvars is None or notplot or downloadonly: + return tvars + + return state_postprocessing(tvars) + + +def state_postprocessing(variables): + """ + Placeholder for State post-processing + """ + return variables + + +def eng(trange=['2020-11-5', '2020-11-6'], + probe='a', + datatype='eng_datatype', + level='l1', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=False): + """ + This function loads data from the Engineering (ENG) + + Parameters + ---------- + trange : list of str + time range of interest [starttime, endtime] with the format + 'YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + probe: str + Spacecraft identifier ('a' or 'b') + + datatype: str + Data type; Valid options: + 'eng_datatype' for L1 data + + level: str + Data level; options: 'l1' (default: l1) + + suffix: str + The tplot variable names will be given this suffix. By default, + no suffix is added. + + get_support_data: bool + Data with an attribute "VAR_TYPE" with a value of "support_data" + will be loaded into tplot. By default, only loads in data with a + "VAR_TYPE" attribute of "data". + + varformat: str + The file variable formats to load into tplot. Wildcard character + "*" is accepted. By default, all variables are loaded in. + + varnames: list of str + List of variable names to load (if not specified, + all data variables are loaded) + + downloadonly: bool + Set this flag to download the CDF files, but not load them into + tplot variables + + notplot: bool + Return the data in hash tables instead of creating tplot variables + + no_update: bool + If set, only load data from your local cache + + time_clip: bool + Time clip the variables to exactly the range specified in the trange keyword + + Returns + ---------- + List of tplot variables created. + + """ + tvars = load(instrument='eng', probe=probe, trange=trange, level=level, datatype=datatype, suffix=suffix, get_support_data=get_support_data, varformat=varformat, varnames=varnames, downloadonly=downloadonly, notplot=notplot, time_clip=time_clip, no_update=no_update) + + if tvars is None or notplot or downloadonly: + return tvars + + return eng_postprocessing(tvars) + + +def eng_postprocessing(variables): + """ + Placeholder for ENG post-processing + """ + return variables + + + diff --git a/pyspedas/elfin/config.py b/pyspedas/elfin/config.py new file mode 100644 index 00000000..a84eeeaa --- /dev/null +++ b/pyspedas/elfin/config.py @@ -0,0 +1,12 @@ +import os + +CONFIG = {'local_data_dir': 'elfin_data/', + 'remote_data_dir': 'https://data.elfin.ucla.edu/'} + +# override local data directory with environment variables +if os.environ.get('SPEDAS_DATA_DIR'): + CONFIG['local_data_dir'] = os.sep.join([os.environ['SPEDAS_DATA_DIR'], 'elfin']) + +if os.environ.get('ELFIN_DATA_DIR'): + CONFIG['local_data_dir'] = os.environ['ELFIN_DATA_DIR'] + \ No newline at end of file diff --git a/pyspedas/elfin/docs/elfin.rst b/pyspedas/elfin/docs/elfin.rst new file mode 100644 index 00000000..71514765 --- /dev/null +++ b/pyspedas/elfin/docs/elfin.rst @@ -0,0 +1,122 @@ +Electron Losses and Fields Investigation (ELFIN) +======================================================================== +The routines in this module can be used to load data from the Electron Losses and Fields Investigation (ELFIN) mission. + + +Fluxgate Magnetometer (FGM) +---------------------------------------------------------- +.. autofunction:: pyspedas.elfin.fgm + +Example +^^^^^^^^^ + +.. code-block:: python + + import pyspedas + from pytplot import tplot + elfin_vars = pyspedas.elfin.fgm(trange=['2020-11-28', '2020-11-28']) + tplot('ela_fgs') + +.. image:: _static/elfin_fgm.png + :align: center + :class: imgborder + + +Energetic Particle Detector (EPD) +---------------------------------------------------------- +.. autofunction:: pyspedas.elfin.epd + +Example +^^^^^^^^^ + +.. code-block:: python + + import pyspedas + from pytplot import tplot + elfin_vars = pyspedas.elfin.epd(trange=['2020-11-28', '2020-11-29']) + tplot('ela_pef') + +.. image:: _static/elfin_epd.png + :align: center + :class: imgborder + + +Magneto Resistive Magnetometer (MRMa) +---------------------------------------------------------- +.. autofunction:: pyspedas.elfin.mrma + +Example +^^^^^^^^^ + +.. code-block:: python + + import pyspedas + from pytplot import tplot + elfin_vars = pyspedas.elfin.mrma(trange=['2020-11-5', '2020-11-6']) + tplot('ela_mrma') + +.. image:: _static/elfin_mrma.png + :align: center + :class: imgborder + + +Magneto Resistive Magnetometer (MRMi) +---------------------------------------------------------- +.. autofunction:: pyspedas.elfin.mrmi + +Example +^^^^^^^^^ + +.. code-block:: python + + import pyspedas + from pytplot import tplot + elfin_vars = pyspedas.elfin.mrmi(trange=['2020-11-5', '2020-11-6']) + tplot('ela_mrmi') + +.. image:: _static/elfin_mrmi.png + :align: center + :class: imgborder + + +State data (state) +---------------------------------------------------------- +.. autofunction:: pyspedas.elfin.state + +Example +^^^^^^^^^ + +.. code-block:: python + + import pyspedas + from pytplot import tplot + elfin_vars = pyspedas.elfin.state(trange=['2020-11-5', '2020-11-6']) + tplot('ela_pos_gei') + +.. image:: _static/elfin_state.png + :align: center + :class: imgborder + + +Engineering (ENG) +---------------------------------------------------------- +.. autofunction:: pyspedas.elfin.eng + +Example +^^^^^^^^^ + +.. code-block:: python + + import pyspedas + from pytplot import tplot + elfin_vars = pyspedas.elfin.eng(trange=['2020-11-5', '2020-11-6']) + tplot('ela_eng_tplot') + +.. image:: _static/elfin_eng.png + :align: center + :class: imgborder + + + + + \ No newline at end of file diff --git a/pyspedas/elfin/eng/__init__.py b/pyspedas/elfin/eng/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyspedas/elfin/eng/eng.py b/pyspedas/elfin/eng/eng.py new file mode 100644 index 00000000..1bcbe2a7 --- /dev/null +++ b/pyspedas/elfin/eng/eng.py @@ -0,0 +1,85 @@ +from .load import load + +def eng(trange=['2020-11-5', '2020-11-6'], + probe='a', + datatype='eng_datatype', + level='l1', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=False): + """ + This function loads data from the Engineering (ENG) + + Parameters + ---------- + trange : list of str + time range of interest [starttime, endtime] with the format + 'YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + probe: str + Spacecraft identifier ('a' or 'b') + + datatype: str + Data type; Valid options: + 'eng_datatype' for L1 data + + level: str + Data level; options: 'l1' (default: l1) + + suffix: str + The tplot variable names will be given this suffix. By default, + no suffix is added. + + get_support_data: bool + Data with an attribute "VAR_TYPE" with a value of "support_data" + will be loaded into tplot. By default, only loads in data with a + "VAR_TYPE" attribute of "data". + + varformat: str + The file variable formats to load into tplot. Wildcard character + "*" is accepted. By default, all variables are loaded in. + + varnames: list of str + List of variable names to load (if not specified, + all data variables are loaded) + + downloadonly: bool + Set this flag to download the CDF files, but not load them into + tplot variables + + notplot: bool + Return the data in hash tables instead of creating tplot variables + + no_update: bool + If set, only load data from your local cache + + time_clip: bool + Time clip the variables to exactly the range specified in the trange keyword + + Returns + ---------- + List of tplot variables created. + + """ + tvars = load(instrument='eng', probe=probe, trange=trange, level=level, datatype=datatype, suffix=suffix, + get_support_data=get_support_data, varformat=varformat, varnames=varnames, downloadonly=downloadonly, + notplot=notplot, time_clip=time_clip, no_update=no_update) + + if tvars is None or notplot or downloadonly: + return tvars + + return eng_postprocessing(tvars) + + +def eng_postprocessing(variables): + """ + Placeholder for ENG post-processing + """ + return variables + diff --git a/pyspedas/elfin/epd/__init__.py b/pyspedas/elfin/epd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyspedas/elfin/epd/calibration_l1.py b/pyspedas/elfin/epd/calibration_l1.py new file mode 100644 index 00000000..c76b321e --- /dev/null +++ b/pyspedas/elfin/epd/calibration_l1.py @@ -0,0 +1,337 @@ +import logging +import pathlib +import importlib.resources +from typing import List, Dict + +import numpy as np +from pyspedas import time_double, time_string +from pytplot import get_timespan, store, get, store_data, get_data + + +def read_epde_calibration_data(path: pathlib.Path) -> List[Dict]: + """ + Read ELFIN EPDE calibration data from file and return + list of parsed calibration datasets. + + Parameters + ---------- + path : pathlib.Path + The path to the calibration data file. + + Returns + ---------- + List of EPDE calibration data parsed from the file. + + """ + + def parse_float_line(line: str): + array_str = line.split(":")[1].strip() + return [float(f.rstrip(".")) for f in array_str.split(",")] + + lines = [] + with open(path, "r") as f: + lines = f.readlines() + + date_lines = (i for i, line in enumerate(lines) if line.startswith("Date")) + calibrations = [] + for i in date_lines: + + date_str = lines[i].split(":")[1].strip() + gf_str = lines[i+1].split(":")[1].strip() + + cal = { + "date": time_double(date_str), + "gf": float(gf_str), + "overaccumulation_factors": parse_float_line(lines[i+2]), + "thresh_factors": parse_float_line(lines[i+3]), + "ch_efficiencies": parse_float_line(lines[i+4]), + "ebins": parse_float_line(lines[i+5]), + } + + calibrations.append(cal) + + return calibrations + + +def get_epde_calibration(calibration_data: Dict) -> Dict: + """ + Performs basic transformations on calibration parameters loaded + from a data file, e.g. calculate channel factors and create energy bin labels. + + Returned dictionary values are (mostly) numpy arrays for later usage. + + Parameters + ---------- + calibration_data : dict + A single set of calibration data read from a file. See `obj`:read_epde_calibration_data: + + Returns + ---------- + Dictionary with EPD gain factor, overaccumulation factors, threshold factors, + channel efficiencies, energy bins (and logmean), calibration channel factors, + and the labels for the energy bins. + + """ + + gf = calibration_data["gf"] + + overaccumulation_factors = np.array(calibration_data["overaccumulation_factors"]) + thresh_factors = np.array(calibration_data["thresh_factors"]) + ch_efficiencies = np.array(calibration_data["ch_efficiencies"]) + ebins = np.array(calibration_data["ebins"]) + + cal_ch_factors = 1.0 / (gf * thresh_factors * ch_efficiencies) + + ebins_logmean = ebins.copy() + for i in range(0, 15): + i_mean_of_logs = (np.log10(ebins[i]) + np.log10(ebins[i+1])) / 2.0 + ebins_logmean[i] = np.power(10.0, i_mean_of_logs) + ebins_logmean[15] = 6500.0 + + ebin_labels = [ + "50-80", "80-120", "120-160", "160-210", + "210-270", "270-345", "345-430", "430-630", + "630-900", "900-1300", "1300-1800", "1800-2500", + "2500-3350", "3350-4150", "4150-5800", "5800+", + ] + + calibration = { + "epd_gf": gf, + "epd_overaccumulation_factors": overaccumulation_factors, + "epd_thresh_factors": thresh_factors, + "epd_ch_efficiencies": ch_efficiencies, + "epd_ebins": ebins, + "epd_cal_ch_factors": cal_ch_factors, + "epd_ebins_logmean": ebins_logmean, + "epd_ebin_labels": ebin_labels, + } + + return calibration + + +def get_epdi_calibration() -> Dict: + """ + Ported from IDL SPEDAS elf_get_epd_calibration procedure. There are + no calibration data files for epdi. Current as of 2023-06-11. + + Vassilis Angelopoulos' most recent comments from IDL procedure reproduced. + + Returns + ---------- + Dictionary with EPD gain factor, overaccumulation factors, threshold factors, + channel efficiencies, energy bins (and logmean), calibration channel factors, + and the labels for the energy bins. + + """ + + # 9deg x 9deg (FWHM Solid Angle) by 1/4" dia. round aperture area + gf = 0.031256 + + overaccumulation_factors = np.ones(16) + thresh_factors = np.ones(16) + thresh_factors[0] = 0.2 + ch_efficiencies = np.ones(16) + + cal_ch_factors = 1.0 / (gf * thresh_factors * ch_efficiencies) + + # in keV based on Jiang Liu's Geant4 code 2019-3-5 + ebins = np.array([ + 50.0, 80.0, 120.0, 160.0, + 210.0, 270.0, 345.0, 430.0, + 630.0, 900.0, 1300.0, 1800.0, + 2500.0, 3350.0, 4150.0, 5800.0, + ]) + + ebins_logmean = ebins.copy() + for i in range(0, 15): + i_mean_of_logs = (np.log10(ebins[i]) + np.log10(ebins[i+1])) / 2.0 + ebins_logmean[i] = np.power(10.0, i_mean_of_logs) + ebins_logmean[15] = 6500.0 + + # used same bins as electrons (dead layer well below 50keV) + ebin_labels = [ + "50-80", "80-120", "120-160", "160-210", + "210-270", "270-345", "345-430", "430-630", + "630-900", "900-1300", "1300-1800", "1800-2500", + "2500-3350", "3350-4150", "4150-5800", "5800+", + ] + + calibration = { + "epd_gf": gf, + "epd_overaccumulation_factors": overaccumulation_factors, + "epd_thresh_factors": thresh_factors, + "epd_ch_efficiencies": ch_efficiencies, + "epd_ebins": ebins, + "epd_cal_ch_factors": cal_ch_factors, + "epd_ebins_logmean": ebins_logmean, + "epd_ebin_labels": ebin_labels, + } + + return calibration + + +def get_epd_calibration(probe, instrument, trange): + + if instrument == "epde": + filename = f"el{probe}_epde_cal_data.txt" + + cal_datasets = None + with importlib.resources.path("pyspedas.elfin.epd", filename) as cal_file_path: + cal_datasets = read_epde_calibration_data(cal_file_path) + + if not cal_datasets: + raise ValueError("Could not load calibration data") + + # Choose the latest calibration data preceeding the start of trange + cal_datasets.sort(key=lambda cal: cal["date"]) + filtered = list(filter(lambda cal: cal["date"] < time_double(trange[0]), cal_datasets)) + epde_cal_file_data = filtered[-1] + + cal_date = time_string(epde_cal_file_data["date"]) + logging.info(f"Using EPDE calibration data from {cal_date}") + + return get_epde_calibration(epde_cal_file_data) + + elif instrument == "epdi": + return get_epdi_calibration() + else: + raise ValueError(f"Unknown instrument: {instrument!r}") + + +def calibrate_epd( + tplotname, + trange=None, + type_="eflux", + probe=None, + deadtime_corr=None, + nspinsinsum=1, +): + """ + Ported from IDL SPEDAS elf_cal_epd procedure. Originally authored + by Colin Wilkins (colinwilkins@ucla.edu). Relevant comments on + calibration details have been reproduced here. + """ + + probe = probe if probe is not None else tplotname[2] + + sc = f"el{probe}" + if "pef" in tplotname: + instrument = "epde" + stype = "pef" + elif "pif" in tplotname: + instrument = "epdi" + stype = "pif" + + d = get(tplotname) + if not d: + return # TODO: Or raise ValueError? + + trange = time_double(trange) if trange is not None else get_timespan(tplotname) + + dspinper = get_data(f"{sc}_{stype}_spinper") + dsectnum = get_data(f"{sc}_{stype}_sectnum") + dnsectors = get_data(f"{sc}_{stype}_nsectors") + + epd_cal = get_epd_calibration(probe=probe, instrument=instrument, trange=trange) + + num_samples = len(d.times) + cal_ch_factors = epd_cal["epd_cal_ch_factors"] + overint_factors = epd_cal["epd_overaccumulation_factors"] + ebins = epd_cal["epd_ebins"] + ebins_logmean = epd_cal["epd_ebins_logmean"] + n_sectors = dnsectors.y + + dE = 1.0e-3 * (ebins[1:16] - ebins[0:15]) # in MeV + dE = np.append(dE, 6.2) + + mytimesra = np.ones(num_samples) + n_energies = len(ebins) + mynrgyra = np.ones(n_energies) + + if deadtime_corr is None: + logging.info("Deadtime correction applied with default deadtime; " + "to not apply set deadtime_corr= 0. or a tiny value, e.g. 1.e-9") + # [default ~ 2% above ~max cps in data (after overaccum. corr.) + # of 125Kcps corresponds to 8.e-6 us peak hold in front preamp] + deadtime_corr = 1.0 / (1.02 * 1.25e5) + + dt = (nspinsinsum * dspinper.y / n_sectors * overint_factors[dsectnum.y]) + dt = dt[:,np.newaxis] @ mynrgyra[np.newaxis] + + if type_ == "raw": + store_data( + tplotname, + data={"x": d.times, "y": d.y, "v": np.arange(16, dtype=np.float32)}, + attr_dict=get_data(tplotname, metadata=True) + ) + elif type_ == "cps": + cps = d.y / dt + tot_cps = np.nansum(cps, axis=1)[:,np.newaxis] @ mynrgyra[np.newaxis,:] + + # Deadtime correction + cps_deadtime_corrected = cps / (1.0 - tot_cps * deadtime_corr) + + # Only reason for negatives is deadtime correction + ineg = np.where(np.nansum(cps_deadtime_corrected, axis=1) < 0.0)[0] + if len(ineg) > 0: + cps_deadtime_corrected[ineg,:] = 0.0 + d3interpol = cps_deadtime_corrected.copy() + d3interpol[1:num_samples-1,:] = (cps_deadtime_corrected[0:num_samples-2,:] + + cps_deadtime_corrected[2:num_samples,:]) / 2.0 + cps_deadtime_corrected[ineg,:] = d3interpol[ineg,:] + + store_data( + tplotname, + data={"x": d.times, "y": cps_deadtime_corrected, "v": ebins_logmean}, + attr_dict=get_data(tplotname, metadata=True) + ) + + elif type_ == "nflux": + cps = d.y / dt + tot_cps = np.nansum(cps, axis=1)[:,np.newaxis] @ mynrgyra[np.newaxis,:] + + # Deadtime correction + cps_deadtime_corrected = cps / (1.0 - tot_cps * deadtime_corr) + + rel_energy_cal = cal_ch_factors / dE + nflux = cps_deadtime_corrected * (mytimesra[:,np.newaxis] @ rel_energy_cal[np.newaxis,:]) + + # Only reason for negatives is deadtime correction + ineg = np.where(np.nansum(nflux, axis=1) < 0.0)[0] + if len(ineg) > 0: + nflux[ineg,:] = 0.0 + d3interpol = nflux.copy() + d3interpol[1:num_samples-1,:] = (nflux[0:num_samples-2,:] + + nflux[2:num_samples,:]) / 2.0 + nflux[ineg,:] = d3interpol[ineg,:] + + store_data( + tplotname, + data={"x": d.times, "y": nflux, "v": ebins_logmean}, + attr_dict=get_data(tplotname, metadata=True) + ) + + elif type_ == "eflux": + cps = d.y / dt + tot_cps = np.nansum(cps, axis=1)[:,np.newaxis] @ mynrgyra[np.newaxis,:] + + # Deadtime correction + cps_deadtime_corrected = cps / (1.0 - tot_cps * deadtime_corr) + + eflux_cal = ebins_logmean * (cal_ch_factors / dE) + eflux = cps_deadtime_corrected * (mytimesra[:,np.newaxis] @ eflux_cal[np.newaxis,:]) + + # Only reason for negatives is deadtime correction + ineg = np.where(np.nansum(eflux, axis=1) < 0.0)[0] + if len(ineg) > 0: + eflux[ineg,:] = 0.0 + d3interpol = eflux.copy() + d3interpol[1:num_samples-1,:] = (eflux[0:num_samples-2,:] + + eflux[2:num_samples,:]) / 2.0 + eflux[ineg,:] = d3interpol[ineg,:] + + store_data( + tplotname, + data={"x": d.times, "y": eflux, "v": ebins_logmean}, + attr_dict=get_data(tplotname, metadata=True) + ) \ No newline at end of file diff --git a/pyspedas/elfin/epd/calibration_l2.py b/pyspedas/elfin/epd/calibration_l2.py new file mode 100644 index 00000000..bc3efc4c --- /dev/null +++ b/pyspedas/elfin/epd/calibration_l2.py @@ -0,0 +1,428 @@ +import bisect +import logging +import numpy as np +from pytplot import get_data, store_data, options +from pytplot.tplot_math import degap + + +def spec_pa_sort( + spec2plot, + pas2plot, +): + """ + Sort pitch angle in ascending order. + If pitch angle are not monotonic, can't plot it with tplot. + + Parameters + ---------- + spec2plot: ndarray + spectogram variable, can be 2d or 3d array. + pas2plot: ndarray + pitch angle variable, should be the same size as spec_var, or the size of a slice of spec_var + + Return + --------- + spec2plot: in sorted order + pas2plot: in sorted order + """ + if len(spec2plot.shape) == 2: + spec_sectNum, spec_paNum = spec2plot.shape + nEngChannel = 1 + + elif len(spec2plot.shape) == 3: + spec_sectNum, spec_paNum, nEngChannel = spec2plot.shape + else: + logging.error("ELF EPD L2 PA SPECTOGRAM: spec2plot is not 2d or 3d!") + return + + if len(pas2plot.shape) == 2: + pa_sectNum, pa_paNum = pas2plot.shape + else: + logging.error("ELF EPD L2 PA SPECTOGRAM: pas2plot is not 2d!") + return + + if (pa_sectNum != spec_sectNum) or (pa_paNum != spec_paNum): + logging.error("ELF EPD L2 PA SPECTOGRAM: pas2plot and spec2plot not same size!") + return + + if len(spec2plot.shape) == 3: + for i in range(spec_sectNum): + line = pas2plot[i, :] + # If the line is in descending order + if np.array_equal(line, np.sort(line)[::-1]): + # Flip the line + pas2plot[i, :] = line[::-1] + for j in range(nEngChannel): + spec2plot[i, :, j] = spec2plot[i, :, j][::-1] + elif len(spec2plot.shape) == 2: + for i in range(spec_sectNum): + line = pas2plot[i, :] + # If the line is in descending order + if np.array_equal(line, np.sort(line)[::-1]): + # Flip the line + pas2plot[i, :] = line[::-1] + spec2plot[i, :] = spec2plot[i, :][::-1] + + return spec2plot, pas2plot + + +def epd_l2_Espectra_option( + flux_var, +): + """ + Add options for omni/para/anti/perp flux spectra + + Parameters + ---------- + flux_var: str + Tplot variable name of 2d omni/para/anti/perp flux spectra + + """ + unit_ = '#/(s-cm$^2$-str-MeV)' if "nflux" in flux_var else 'keV/(s-cm$^2$-str-MeV)' + zrange = [10, 2e7] if "nflux" in flux_var else [1.e1, 2.e7] + + options(flux_var, 'spec', True) + options(flux_var, 'yrange', [55., 6800]) # energy range + options(flux_var, 'zrange', zrange) + options(flux_var, 'ylog', True) + options(flux_var, 'zlog', True) + flux_var_ytitle = flux_var[0:10] + "\n" + flux_var[11:] + options(flux_var, 'ytitle', flux_var_ytitle) + options(flux_var, 'ysubtitle', '[keV]') + options(flux_var, 'ztitle', unit_) + + +def epd_l2_Espectra( + flux_tvar, + LC_tvar, + LCfatol=None, + LCfptol=None, + nodegap=True, + ): + + """ + Produce omni/para/perp/anti flux spectra for ELF EPD L2 data + + Parameters + ---------- + flux_tvar: str + Tplot variable name of a 3d energy time spectra + LC_tvar: str + Tplot variable name of loss cone + LCfatol: float, optional + Tolerance angle for para and anti flux. A positive value makes the loss + cone/antiloss cone smaller by this amount. + Default is 22.25 deg. + LCfptol: float, optional + Tolerance angle for perp flux. A negative value means a wider angle for + perp flux. + Default is -11 deg. + Nodegap: bool, optional + Flag for degap. If set, skip degap. + Default is False. + + Return + ---------- + List of four elements. + omni_var: str. Tplot variable name of 2d omni spectra + para_var: str. Tplot variable name of 2d parallel spectra + perp_var: str. Tplot variable name of 2d perpendicualr spectra + anti_var: str. Tplot variable name of 2d antiparallel spectra + """ + + # load L2 t-PA-E 3D flux + data = get_data(flux_tvar) + + # parameters setup + nspinsavailable, nPAsChannel, nEngChannel= np.shape(data.y) + if 'hs' in flux_tvar: + nspinsectors = (nPAsChannel - 2)*2 + elif 'fs' in flux_tvar: + nspinsectors = (nPAsChannel - 2) + else: + logging.error("ELF EPD L2 ENERGY SPECTROGRAM: invalid input tplot variable!") + + FOVo2 = 11. # Field of View divided by 2 (deg) + dphsect = 360./nspinsectors + SectWidtho2 = dphsect/2. + LCfatol = FOVo2 + SectWidtho2 if LCfatol is None else LCfatol # tolerance of pitch angle in field aligned direction, default 22.25 deg + LCfptol = -FOVo2 if LCfptol is None else LCfptol # tolerance of pitch angel in perpendicular direction, default -11. deg + # TODO: make sure these default values are correct + + # calculate domega in PA + pas2plot = np.copy(data.v1) + spec2plot = np.copy(data.y) + energy = np.copy(data.v2) + pas2plot_domega = (2. * np.pi / nspinsectors) * np.sin(np.pi * pas2plot / 180.) + index1, index2 = np.where((pas2plot < 360. / nspinsectors / 2) | (pas2plot > 180. - 360. / nspinsectors / 2)) + pas2plot_domega[index1, index2] = (np.pi/nspinsectors)*np.sin(np.pi/nspinsectors) + + # # pas2plot_bcast need to have the same nan as spec2plot. + # if not, np.nansum(pas2plot_bcast, axis=1) will be wrong + pas2plot_domega[:, 0] = np.nan + pas2plot_domega[:, -1] = np.nan + #pas2plot_repeat = np.repeat(pas2plot[:, :, np.newaxis], nEngChannel, axis=2) + pas2plot_domega_bcast = np.broadcast_to(pas2plot_domega[:, :, np.newaxis], (nspinsavailable, nPAsChannel, nEngChannel)) + + #=========================== + # OMNI FLUX + #=========================== + # calculate omni flux + Espectra_omni = np.nansum(spec2plot * pas2plot_domega_bcast, axis=1) / np.nansum(pas2plot_domega_bcast, axis=1) + + # output omni tvar + omni_var = f"{flux_tvar.replace('Epat_','')}_omni" + store_data(omni_var, data={'x': data.times, 'y': Espectra_omni, 'v': energy}, attr_dict=get_data(flux_tvar, metadata=True)) + epd_l2_Espectra_option(omni_var) + + #=========================== + # PARA FLUX + #=========================== + # load loss cone data + data = get_data(LC_tvar) + + # southern hemisphere + paraedgedeg = np.array([lc if lc < 90 else 180-lc for lc in data.y]) + paraedgedeg_bcast = np.broadcast_to(paraedgedeg[:, np.newaxis], (nspinsavailable, nPAsChannel)) + + # select index + iparapas, jparapas = np.where(pas2plot< paraedgedeg_bcast-LCfatol) + spec2plot_allowable = np.zeros((nspinsavailable, nPAsChannel, nEngChannel)) + spec2plot_allowable[iparapas, jparapas, :] = 1 + Espectra_para = np.nansum(spec2plot * pas2plot_domega_bcast * spec2plot_allowable, axis=1) / np.nansum(pas2plot_domega_bcast * spec2plot_allowable, axis=1) + + # output para tvar + para_var = f"{flux_tvar.replace('Epat_','')}_para" + store_data(para_var, data={'x': data.times, 'y': Espectra_para, 'v': energy}, attr_dict=get_data(flux_tvar, metadata=True)) + epd_l2_Espectra_option(para_var) + + #=========================== + # ANTI FLUX + #=========================== + # select index + iantipas, jantipas = np.where(pas2plot > 180+LCfatol-paraedgedeg_bcast) + spec2plot_allowable = np.zeros((nspinsavailable, nPAsChannel, nEngChannel)) + spec2plot_allowable[iantipas, jantipas, :] = 1 + Espectra_anti = np.nansum(spec2plot * pas2plot_domega_bcast * spec2plot_allowable, axis=1) / np.nansum(pas2plot_domega_bcast * spec2plot_allowable, axis=1) + + # output anti tvar + anti_var = f"{flux_tvar.replace('Epat_','')}_anti" + store_data(anti_var, data={'x': data.times, 'y': Espectra_anti, 'v': energy}, attr_dict=get_data(flux_tvar, metadata=True)) + epd_l2_Espectra_option(anti_var) + + #=========================== + # PERP FLUX + #=========================== + # select index + iperppas, jperppas = np.where( + (pas2plot < 180-LCfptol-paraedgedeg_bcast) & (pas2plot > LCfptol+paraedgedeg_bcast)) + spec2plot_allowable = np.zeros((nspinsavailable, nPAsChannel, nEngChannel)) + spec2plot_allowable[iperppas, jperppas, :] = 1 + Espectra_perp = np.nansum(spec2plot * pas2plot_domega_bcast * spec2plot_allowable, axis=1) / np.nansum(pas2plot_domega_bcast * spec2plot_allowable, axis=1) + + # output anti tvar + perp_var = f"{flux_tvar.replace('Epat_','')}_perp" + store_data(perp_var, data={'x': data.times, 'y': Espectra_perp, 'v': energy}, attr_dict=get_data(flux_tvar, metadata=True)) + epd_l2_Espectra_option(perp_var) + + #=========================== + # DEGAP + #=========================== + if nodegap is False: + nspinsinsum = get_data(f"{flux_tvar[0:3]}_pef_nspinsinsum") + spinper = get_data(f"{flux_tvar[0:3]}_pef_tspin") + if np.average(nspinsinsum.y) > 1 : + mydt = np.max(nspinsinsum.y)*np.median(spinper.y) + else: + mydt = np.median(spinper.y) + + degap(omni_var, dt=mydt, margin=0.5*mydt/2) + degap(para_var, dt=mydt, margin=0.5*mydt/2) + degap(anti_var, dt=mydt, margin=0.5*mydt/2) + degap(perp_var, dt=mydt, margin=0.5*mydt/2) + + + return [omni_var, para_var, anti_var, perp_var] + + +def epd_l2_PAspectra_option( + flux_var, + set_zrange = True, +): + """ + Add options for pitch angle spectra + + Parameters + ---------- + flux_var: str + Tplot variable name of 2d pitch angle spectra + + set_zrange: bool, optonal + Default is True. Only works for default four channels. + """ + + unit_ = '#/(s-\ncm$^2$-str-MeV)' if "nflux" in flux_var else 'keV/(s-\ncm$^2$-str-MeV)' + if set_zrange is True: + if "nflux" in flux_var: + zrange_list = { + 0: [2.e3, 2.e7], + 1: [1.e3, 4.e6], + 2: [1.e2, 1.e6], + 3: [1.e1, 2.e4], + } + else: + zrange_list = { + 0: [5.e5, 1.e10], + 1: [5.e5, 5.e9], + 2: [5.e5, 2.5e9], + 3: [1.e5, 1.e8], + } + ch_num = int(flux_var.split("ch")[-1]) + zrange = zrange_list.get(ch_num, [1e1, 1e2]) if "nflux" in flux_var else zrange_list.get(ch_num, [1.e4, 1.e7]) + options(flux_var, 'zrange', zrange) + + options(flux_var, 'spec', True) + #options(flux_var, 'yrange', [-180, 180]) # energy range + options(flux_var, 'ylog', False) + options(flux_var, 'zlog', True) + flux_var_ytitle = flux_var[0:10] + "\n" + flux_var[11:] + options(flux_var, 'ytitle', flux_var_ytitle) + options(flux_var, 'ysubtitle', 'PA (deg)') + options(flux_var, 'ztitle', unit_) + + +def epd_l2_PAspectra( + flux_tvar, + energybins = None, + energies = None, + nodegap=True, + ): + """ + This function processes a 3D energy-time spectra from ELF EPD L2 data + to produce pitch angle spectra, applying either specified energy bins + or a defined energy range. + + Parameters + ---------- + flux_tvar: str + Tplot variable name of a 3d energy time spectra + + energybins: list of int, optional + Specified the energy bins used for generating pitch angle spectra. + Default is [(0,2),(3,5), (6,8), (9,15)], which bins energy as follows: + ch0: Energy channels 0-2, + ch1: Energy channels 3-5, + ch2: Energy channels 6-8, + ch3: Energy channels 9-15 + If both 'energybins' and 'energies' are set, 'energybins' takes precedence + energy + + energies: list of tuple of float, optional + Specifies the energy range for each bin in the pitch angle spectra. + Example: energies=[(50.,160.),(160.,345.),(345.,900.),(900.,7000.)] + If both 'energybins' and 'energies' are set, 'energybins' takes precedence. + Energy and energybin table: + channel energy_range energy_midbin + 0 50-80 63.2 + 1 80-120 97.9 + 2 120-160 138.5 + 3 160-210 183.3 + 4 210-270 238.1 + 5 270-345 305.2 + 6 345-430 385.1 + 7 430-630 520.4 + 8 630-900 752.9 + 9 900-1300 1081.6 + 10 1300-1800 1529.7 + 11 1800-2500 2121.3 + 12 2500-3350 2893.9 + 13 3350-4150 3728.6 + 14 4150-5800 4906.1 + 15 5800+ 6500.0 + Nodegap: bool, optional + Flag for degap. If set, skip degap. + Default is False. + + Return + ---------- + List of tplot variable names for pitch angle spectra + """ + # constant energy range + EMINS = np.array([ + 50.000008, 79.999962, 120.00005, 159.99998, 210.00015, 269.99973, + 345.00043, 429.99945, 630.00061, 899.99890, 1300.0013, 1799.9985, + 2500.0022, 3349.9990, 4150.0034, 5800.0000]) # TODO: make sure it's safe to hardcode here + EMAXS = np.array([ + 79.999962, 120.00005, 159.99998, 210.00015, 269.99973, 345.00043, + 429.99945, 630.00061, 899.99890, 1300.0013, 1799.9985, 2500.0022, + 3349.9990, 4150.0034, 5800.0000, 7200.0000]) + + # load L2 t-PA-E 3D flux + data = get_data(flux_tvar) + energy_midbin = np.copy(data.v2) + pas2plot = np.copy(data.v1) + spec2plot = np.copy(data.y) + nspinsavailable, nPAsChannel, nEngChannel= np.shape(data.y) + #nspinsectors = (nPAsChannel - 2)*2 + + spec2plot, pas2plot = spec_pa_sort(spec2plot, pas2plot) + + # get energy bin for pitch angle spectra + if energybins is not None: + MinE_channels, MaxE_channels = zip(*energybins) + MinE_channels = list(MinE_channels) + MaxE_channels = list(MaxE_channels) + if energies is not None: + logging.warning("both energis and energybins are set, 'energybins' takes precedence!") + elif energies is not None: + MinE_channels = [bisect.bisect_left(energy_midbin, min_energy) for min_energy, max_energy in energies] + MaxE_channels = [bisect.bisect_right(energy_midbin, max_energy) - 1 for min_energy, max_energy in energies] + else: + MinE_channels = [0, 3, 6, 9] + MaxE_channels = [2, 5, 8, 15] + logging.info(f"Energy channel {list(zip(MinE_channels, MaxE_channels))} are used for epd l2 pitch angle spectra.") + + # broadcast Emax and Emin so that they have the same size of spec2plot + Emaxs_bcast = np.broadcast_to(EMAXS[np.newaxis, np.newaxis, :], (nspinsavailable, nPAsChannel, nEngChannel)) + Emins_bcast = np.broadcast_to(EMINS[np.newaxis, np.newaxis, :], (nspinsavailable, nPAsChannel, nEngChannel)) + + # define PAspectra, size is time * pa * energy channel + numchannels = len(MinE_channels) + PAspectra = np.full((nspinsavailable, nPAsChannel, numchannels), np.nan) + PA_tvars = [] + + # loop over specified energy channels + for ichannel in range(numchannels): + min_channel = MinE_channels[ichannel] + max_channel = MaxE_channels[ichannel] + + PAspectra_single = np.nansum(spec2plot[:,:,min_channel:max_channel+1] * \ + (Emaxs_bcast[:,:,min_channel:max_channel+1] - Emins_bcast[:,:,min_channel:max_channel+1]), axis=2) \ + / np.nansum(Emaxs_bcast[:,:,min_channel:max_channel+1] - Emins_bcast[:,:,min_channel:max_channel+1], axis=2) + + # make the first and last pa nan, for the comparison with idl + PAspectra_single[:, 0] = np.nan + PAspectra_single[:, -1] = np.nan + + # output tvariable + PA_var = f"{flux_tvar.replace('Epat_','')}_ch{ichannel}" + + store_data(PA_var, data={'x': data.times, 'y': PAspectra_single, 'v': pas2plot}) + epd_l2_PAspectra_option(PA_var) + + #=========================== + # DEGAP + #=========================== + if nodegap is False: + nspinsinsum = get_data(f"{flux_tvar[0:3]}_pef_nspinsinsum") + spinper = get_data(f"{flux_tvar[0:3]}_pef_tspin") + if np.average(nspinsinsum.y) > 1 : + mydt = np.max(nspinsinsum.y)*np.median(spinper.y) + else: + mydt = np.median(spinper.y) + degap(PA_var, dt=mydt, margin=0.5*mydt/2) + + PA_tvars.append(PA_var) + + + return PA_tvars \ No newline at end of file diff --git a/pyspedas/elfin/epd/ela_epde_cal_data.txt b/pyspedas/elfin/epd/ela_epde_cal_data.txt new file mode 100644 index 00000000..2e42a7fc --- /dev/null +++ b/pyspedas/elfin/epd/ela_epde_cal_data.txt @@ -0,0 +1,8 @@ +; ELFIN-A EPDE Calibration Data + +Date: 2018-09-17/00:00:00 +gf: 0.15 +overaccumulation_factors: 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.15 +thresh_factors: .2, 1., 1.2, 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1. +ch_efficiencies: 0.74, 0.8, 0.85, 0.86, 0.87, 0.87, 0.87, 0.87, 0.82, 0.8, 0.75, 0.6, 0.5, 0.45, 0.25, 0.05 +ebins: 50., 80., 120., 160., 210., 270., 345., 430., 630., 900., 1300., 1800., 2500., 3350., 4150., 5800. diff --git a/pyspedas/elfin/epd/elb_epde_cal_data.txt b/pyspedas/elfin/epd/elb_epde_cal_data.txt new file mode 100644 index 00000000..014e4c3f --- /dev/null +++ b/pyspedas/elfin/epd/elb_epde_cal_data.txt @@ -0,0 +1,19 @@ +; ELFIN-B EPDE Calibration Data +Date: 2018-09-17/00:00:00 +gf: 0.15 +overaccumulation_factors: 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.15 +thresh_factors: .0325, .208, .156, .13, .13, .13, .13, .13, .13, .13, .13, .13, .13, .13, .13, .13 +ch_efficiencies: 0.74, 0.8, 0.85, 0.86, 0.87, 0.87, 0.87, 0.87, 0.82, 0.8, 0.75, 0.6, 0.5, 0.45, 0.25, 0.05 +ebins: 50., 80., 120., 160., 210., 270., 345., 430., 630., 900., 1300., 1800., 2500., 3350., 4150., 5800. +Date: 2020-05-30/00:00:00 +gf: 0.15 +overaccumulation_factors: 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.15 +thresh_factors: .13, .208, .156, .13, .13, .13, .13, .13, .13, .13, .13, .13, .13, .13, .13, .13 +ch_efficiencies: 0.74, 0.8, 0.85, 0.86, 0.87, 0.87, 0.87, 0.87, 0.82, 0.8, 0.75, 0.6, 0.5, 0.45, 0.25, 0.05 +ebins: 50., 80., 120., 160., 210., 270., 345., 430., 630., 900., 1300., 1800., 2500., 3350., 4150., 5800. +Date: 2020-08-20/10:00:00 +gf: 0.15 +overaccumulation_factors: 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.15 +thresh_factors: 1.3., 2.08, 1.56, 1.3, 1.3, 1.3, 1.3, 1.3, 1.3, 1.3, 1.3, 1.3, 1.3, 1.3, 1.3, 1.3 +ch_efficiencies: 0.74, 0.8, 0.85, 0.86, 0.87, 0.87, 0.87, 0.87, 0.82, 0.8, 0.75, 0.6, 0.5, 0.45, 0.25, 0.05 +ebins: 50., 80., 120., 160., 210., 270., 345., 430., 630., 900., 1300., 1800., 2500., 3350., 4150., 5800. diff --git a/pyspedas/elfin/epd/epd.py b/pyspedas/elfin/epd/epd.py new file mode 100644 index 00000000..0a5c3be3 --- /dev/null +++ b/pyspedas/elfin/epd/epd.py @@ -0,0 +1,193 @@ +import logging +from pytplot import get_data +import numpy as np + +from pyspedas.elfin.load import load +from pyspedas.elfin.epd.postprocessing import epd_l1_postprocessing, epd_l2_postprocessing + +def elfin_load_epd(trange=['2020-11-01', '2020-11-02'], + probe='a', + datatype='pef', + level='l1', + type_='nflux', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=True, + nspinsinsum=None, + fullspin=False, + PAspec_energies=None, + PAspec_energybins=None, + Espec_LCfatol=None, + Espec_LCfptol=None, +): + """ + This function loads data from the Energetic Particle Detector (EPD) + + Parameters + ---------- + trange : list of str + time range of interest [starttime, endtime] with the format + 'YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + probe: str + Spacecraft identifier ('a' or 'b') + + datatype: str + Data type; Valid options: + 'pef' for L1 data + 'pif' for L1 data + 'pes' for L1 data + 'pis' for L1 data + + level: str + Data level; options: 'l1' (default: l1) + + type_ : str + Calibrated data type, one of ('raw', 'cps', 'nflux', 'eflux'). ('eflux' not fully tested) + Default: 'nflux'. + + suffix: str + The tplot variable names will be given this suffix. By default, + no suffix is added. + + get_support_data: bool + Data with an attribute "VAR_TYPE" with a value of "support_data" + will be loaded into tplot. By default, only loads in data with a + "VAR_TYPE" attribute of "data". + + varformat: str + The file variable formats to load into tplot. Wildcard character + "*" is accepted. By default, all variables are loaded in. + + varnames: list of str + List of variable names to load (if not specified, + all data variables are loaded) + + downloadonly: bool + Set this flag to download the CDF files, but not load them into + tplot variables + + notplot: bool + Return the data in hash tables instead of creating tplot variables + + no_update: bool + If set, only load data from your local cache + + time_clip: bool + Time clip the variables to exactly the range specified in the trange keyword + + nspinsinsum : int, optional + Number of spins in sum which is needed by the calibration function. + + fullspin: bool + If true, generate full spin with l2 epd instead of half spin + Default is False + + PAspec_energybins: list of tuple of int, optional + Specified the energy bins used for generating l2 pitch angle spectra. + Default is [(0,2),(3,5), (6,8), (9,15)]. If both 'PAspec_energybins' and 'PAspec_energies' + are set, 'energybins' takes precedence + + PAspec_energies: list of tuple of float, optional + Specifies the energy range for each bin in the l2 pitch angle spectra. + Example: energies=[(50.,160.),(160.,345.),(345.,900.),(900.,7000.)] + If both 'energybins' and 'energies' are set, 'energybins' takes precedence. + Energy and energybin table: + channel energy_range energy_midbin + 0 50-80 63.2 + 1 80-120 97.9 + 2 120-160 138.5 + 3 160-210 183.3 + 4 210-270 238.1 + 5 270-345 305.2 + 6 345-430 385.1 + 7 430-630 520.4 + 8 630-900 752.9 + 9 900-1300 1081.6 + 10 1300-1800 1529.7 + 11 1800-2500 2121.3 + 12 2500-3350 2893.9 + 13 3350-4150 3728.6 + 14 4150-5800 4906.1 + 15 5800+ 6500.0 + + Espec_LCfatol: float, optional + Tolerance angle for para and anti flux. A positive value makes the loss + cone/antiloss cone smaller by this amount. + Default is 22.25 deg. + + Espec_LCfptol: float, optional + Tolerance angle for perp flux. A negative value means a wider angle for + perp flux. + Default is -11 deg. + + Returns + ---------- + List of tplot variables created. + + """ + logging.info("ELFIN EPD: START LOADING.") + + tvars = load(instrument='epd', probe=probe, trange=trange, level=level, datatype=datatype, suffix=suffix, + get_support_data=get_support_data, varformat=varformat, varnames=varnames, downloadonly=downloadonly, + notplot=notplot, time_clip=time_clip, no_update=no_update) + + logging.info("ELFIN EPD: LOADING END.") + if tvars is None or notplot or downloadonly: + return tvars + + CALIBRATED_TYPE_UNITS = { + "raw": "counts/sector", + "cps": "counts/s", + "nflux": "#/(s-cm$^2$-str-MeV)", + "eflux": "keV/(s-cm$^2$-str-MeV)0", + } + + if type_ in ("cal", "calibrated") or type_ not in CALIBRATED_TYPE_UNITS.keys(): + type_ = "nflux" + + if level == "l1": + l1_tvars = epd_l1_postprocessing(tvars, trange=trange, type_=type_, nspinsinsum=nspinsinsum, + unit=CALIBRATED_TYPE_UNITS[type_]) + return l1_tvars + + elif level == "l2": + logging.info("ELFIN EPD L2: START PROCESSING.") + # check whether input type is allowed + if type_ not in ("nflux","eflux"): + logging.warning(f"fluxtype {type_} is not allowed in l2 data, change to nflux!") + type_ = "nflux" + + res = 'hs' if fullspin is False else 'fs' + + # if 32 sector data is needed, pass the variables with 32 + tvars_32 = [tvar for tvar in tvars if '_32' in tvar] + tvars_16 = [tvar.replace('_32', '') for tvar in tvars_32] + tvars_other = list(set(tvars) - set(tvars_32) - set(tvars_16)) + + tvars_input = tvars_other + tvars_16 + if len(tvars_32) != 0 : + data = get_data(tvars_32[0]) + if np.any(~np.isnan(data.y)) : # if any 32 sector data is not nan + tvars_input = tvars_other + tvars_32 + + l2_tvars = epd_l2_postprocessing( + tvars_input, + fluxtype=type_, + res=res, + PAspec_energies=PAspec_energies, + PAspec_energybins=PAspec_energybins, + Espec_LCfatol=Espec_LCfatol, + Espec_LCfptol=Espec_LCfptol,) + + return l2_tvars + else: + raise ValueError(f"Unknown level: {level}") + + return tvars diff --git a/pyspedas/elfin/epd/postprocessing.py b/pyspedas/elfin/epd/postprocessing.py new file mode 100644 index 00000000..63cc644e --- /dev/null +++ b/pyspedas/elfin/epd/postprocessing.py @@ -0,0 +1,198 @@ +import logging + +from pytplot import get, store, del_data, tnames, tplot_rename, options, tplot +from pyspedas.analysis.time_clip import time_clip as tclip + +from pyspedas.elfin.epd.calibration_l2 import epd_l2_Espectra, epd_l2_PAspectra +from pyspedas.elfin.epd.calibration_l1 import calibrate_epd + +def epd_l1_postprocessing( + tplotnames, + trange=None, + type_=None, + nspinsinsum=None, + unit=None, +): + """ + Calibrates data from the Energetic Particle Detector (EPD) and sets dlimits. + + Parameters + ---------- + tplotnames : list of str + The tplot names of EPD data to be postprocessed. + + trange : list of str + Time range of interest [starttime, endtime] with the format + ['YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + type_ : str, optional + Desired data type. Options: 'raw', 'cps', 'nflux', 'eflux'. + + nspinsinsum : int, optional + Number of spins in sum which is needed by the calibration function. + + unit : str, optional + Units of the data. + + + Returns + ---------- + List of tplot variables created. + + """ + + tplotnames = tplotnames.copy() + + # Calibrate spinper to turn it to seconds + FGM_SAMPLE_RATE = 80.0 + + for name in filter(lambda n: "spinper" in n, tplotnames): + d = get(name) + cal_spinper = d.y / FGM_SAMPLE_RATE + store(name, {"x": d.times, "y": cal_spinper}, metadata=get(name, metadata=True)) + + if nspinsinsum is None: + tn = tnames("*nspinsinsum*") + if tn: + nspin = get(tn[0]) + nspinsinsum = nspin.y if nspin is not None else 1 + else: + nspinsinsum = 1 + + new_tvars = [] + + for name in tplotnames: + if "energies" in name: + del_data(name) + continue + if "sectnum" in name: + options(name, 'ytitle', name) + new_tvars.append(name) + continue + if "spinper" in name: + options(name, 'ytitle', name) + options(name, 'ysubtitle','[sec]') + new_tvars.append(name) + continue + if "nspinsinsum" in name: + options(name, 'ytitle', name) + new_tvars.append(name) + continue + if "nsectors" in name: + options(name, 'ytitle', name) + new_tvars.append(name) + continue + + new_name = f"{name}_{type_}" + tplot_rename(name, new_name) + options(new_name, 'ytitle', new_name) # set units for elx_pef variable + options(new_name, 'ysubtitle', unit) + new_tvars.append(new_name) + + calibrate_epd(new_name, trange=trange, type_=type_, nspinsinsum=nspinsinsum) + + # TODO: Set units and tplot options (obey no_spec) + + return new_tvars + + +def epd_l2_postprocessing( + tplotnames, + fluxtype='nflux', + res='hs', + datatype='e', + PAspec_energies = None, + PAspec_energybins = None, + Espec_LCfatol = None, + Espec_LCfptol = None, +): + """ + Process ELF EPD L2 data and generate omni, para, anti, perp flux spectra. + + Parameters + ---------- + tplotnames : list of str + The tplot names of EPD data to be postprocessed. + + fluxtype: str, optional + Type of flux spectra. + Options: 'nflux' for number flux, 'eflux' for energy flux. + Default is 'nflux'. + + res: str, optional + Resolution of spectra. + Options: 'hs' for half spin, 'fs' for full spin. + Default is 'hs'. + + datatype: str, optional + Type of data. + Options: 'e' for electron data, 'i' for ion data + Default is 'e'. + + PAspec_energybins: list of tuple of int, optional + Specified the energy bins used for generating l2 pitch angle spectra. + Default is [(0,2),(3,5), (6,8), (9,15)]. If both 'PAspec_energybins' and 'PAspec_energies' + are set, 'energybins' takes precedence + + PAspec_energies: list of tuple of float, optional + Specifies the energy range for each bin in the l2 pitch angle spectra. + Example: energies=[(50.,160.),(160.,345.),(345.,900.),(900.,7000.)] + If both 'energybins' and 'energies' are set, 'energybins' takes precedence. + Energy and energybin table: + channel energy_range energy_midbin + 0 50-80 63.2 + 1 80-120 97.9 + 2 120-160 138.5 + 3 160-210 183.3 + 4 210-270 238.1 + 5 270-345 305.2 + 6 345-430 385.1 + 7 430-630 520.4 + 8 630-900 752.9 + 9 900-1300 1081.6 + 10 1300-1800 1529.7 + 11 1800-2500 2121.3 + 12 2500-3350 2893.9 + 13 3350-4150 3728.6 + 14 4150-5800 4906.1 + 15 5800+ 6500.0 + + Espec_LCfatol: float, optional + Tolerance angle for para and anti flux. A positive value makes the loss + cone/antiloss cone smaller by this amount. + Default is 22.25 deg. + + Espec_LCfptol: float, optional + Tolerance angle for perp flux. A negative value means a wider angle for + perp flux. + Default is -11 deg. + + Returns + ---------- + List of tplot variables created. + """ + + tplotnames = tplotnames.copy() + + flux_tname = [name for name in tplotnames if f"p{datatype}f_{res}_Epat_{fluxtype}" in name] + LC_tname = [name for name in tplotnames if f"_{res}_LCdeg" in name] + + if len(flux_tname) != 1: + logging.error(f'{len(flux_tname)} flux tplot variables is found!') + return + + if len(LC_tname) != 1: + logging.error(f'{len(LC_tname)} LC tplot variables is found!') + return + + logging.info("ELFIN EPD L2: START ENERGY SPECTOGRAM.") + # get energy spectra in four directions + E_tvar = epd_l2_Espectra(flux_tname[0], LC_tname[0], LCfatol=Espec_LCfatol, LCfptol=Espec_LCfptol) + + logging.info("ELFIN EPD L2: START PITCH ANGLE SPECTOGRAM.") + # get pitch angle spectra + #PA_tvar = epd_l2_PAspectra(flux_tname[0], energies=[(60, 200),(300, 1000)]) + PA_tvar = epd_l2_PAspectra(flux_tname[0], energies=PAspec_energies, energybins=PAspec_energybins) + + return E_tvar + PA_tvar \ No newline at end of file diff --git a/pyspedas/elfin/fgm/__init__.py b/pyspedas/elfin/fgm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyspedas/elfin/fgm/fgm.py b/pyspedas/elfin/fgm/fgm.py new file mode 100644 index 00000000..e5ee13a5 --- /dev/null +++ b/pyspedas/elfin/fgm/fgm.py @@ -0,0 +1,87 @@ +from .load import load + + +def fgm(trange=['2020-10-01', '2020-10-02'], + probe='a', + datatype='survey', + level='l1', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=False): + """ + This function loads data from the Fluxgate Magnetometer (FGM) + + Parameters + ---------- + trange : list of str + time range of interest [starttime, endtime] with the format + 'YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + probe: str + Spacecraft identifier ('a' or 'b') + + datatype: str + Data type; Valid options: + 'fast', 'survey' for L1 data + + level: str + Data level; options: 'l1' (default: l1) + + suffix: str + The tplot variable names will be given this suffix. By default, + no suffix is added. + + get_support_data: bool + Data with an attribute "VAR_TYPE" with a value of "support_data" + will be loaded into tplot. By default, only loads in data with a + "VAR_TYPE" attribute of "data". + + varformat: str + The file variable formats to load into tplot. Wildcard character + "*" is accepted. By default, all variables are loaded in. + + varnames: list of str + List of variable names to load (if not specified, + all data variables are loaded) + + downloadonly: bool + Set this flag to download the CDF files, but not load them into + tplot variables + + notplot: bool + Return the data in hash tables instead of creating tplot variables + + no_update: bool + If set, only load data from your local cache + + time_clip: bool + Time clip the variables to exactly the range specified in the trange keyword + + Returns + ---------- + List of tplot variables created. + + """ + tvars = load(instrument='fgm', probe=probe, trange=trange, level=level, + datatype=datatype, suffix=suffix, get_support_data=get_support_data, + varformat=varformat, varnames=varnames, downloadonly=downloadonly, + notplot=notplot, time_clip=time_clip, no_update=no_update) + + if tvars is None or notplot or downloadonly: + return tvars + + return fgm_postprocessing(tvars) + + +def fgm_postprocessing(variables): + """ + Placeholder for FGM post-processing + """ + return variables + diff --git a/pyspedas/elfin/load.py b/pyspedas/elfin/load.py new file mode 100644 index 00000000..81e0d8cb --- /dev/null +++ b/pyspedas/elfin/load.py @@ -0,0 +1,85 @@ +from pyspedas.utilities.dailynames import dailynames +from pyspedas.utilities.download import download +from pyspedas.analysis.time_clip import time_clip as tclip +from pytplot import cdf_to_tplot + +from .config import CONFIG + + +def load(trange=['2020-11-5', '2020-11-6'], + probe='a', + instrument='fgm', + datatype='flux', + level='l2', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=False): + """ + This function loads data from the ELFIN mission; this function is not meant + to be called directly; instead, see the wrappers: + + pyspedas.elfin.fgm + pyspedas.elfin.epd + pyspedas.elfin.mrma + pyspedas.elfin.mrmi + pyspedas.elfin.state + pyspedas.elfin.eng + + This is a test + """ + + if instrument == 'fgm': + datatype_str = 'fgs' + if datatype.lower() == 'fast': + datatype_str = 'fgf' + pathformat = f'el{probe}/{level}/{instrument}/{datatype}/%Y/el{probe}_{level}_{datatype_str}_%Y%m%d_v??.cdf' + elif instrument == 'epd': + if datatype == 'pef': + pathformat = f'el{probe}/{level}/{instrument}/fast/electron/%Y/el{probe}_{level}_epdef_%Y%m%d_v??.cdf' + elif datatype == 'pif': + pathformat = f'el{probe}/{level}/{instrument}/fast/ion/%Y/el{probe}_{level}_epdif_%Y%m%d_v??.cdf' + elif datatype == 'pes': + pathformat = f'el{probe}/{level}/{instrument}/survey/electron/%Y/el{probe}_{level}_epdes_%Y%m%d_v??.cdf' + elif datatype == 'pis': + pathformat = f'el{probe}/{level}/{instrument}/survey/ion/%Y/el{probe}_{level}_epdis_%Y%m%d_v??.cdf' + elif instrument == 'mrma': + pathformat = f'el{probe}/{level}/{instrument}/%Y/el{probe}_{level}_{instrument}_%Y%m%d_v??.cdf' + elif instrument == 'mrmi': + pathformat = f'el{probe}/{level}/{instrument}/%Y/el{probe}_{level}_{instrument}_%Y%m%d_v??.cdf' + elif instrument == 'state': + pathformat = f'el{probe}/{level}/{instrument}/{datatype}/%Y/el{probe}_{level}_{instrument}_{datatype}_%Y%m%d_v??.cdf' + elif instrument == 'eng': + pathformat = f'el{probe}/{level}/{instrument}/%Y/el{probe}_{level}_{instrument}_%Y%m%d_v??.cdf' + + # find the full remote path names using the trange + remote_names = dailynames(file_format=pathformat, trange=trange) + + out_files = [] + + files = download(remote_file=remote_names, remote_path=CONFIG['remote_data_dir'], local_path=CONFIG['local_data_dir'], no_download=no_update, last_version=True) + + if files is not None: + for file in files: + out_files.append(file) + + out_files = sorted(out_files) + + if downloadonly: + return out_files + + tvars = cdf_to_tplot(out_files, suffix=suffix, get_support_data=get_support_data, varformat=varformat, + varnames=varnames, notplot=notplot) + + if notplot: + return tvars + + if time_clip: + for new_var in tvars: + tclip(new_var, trange[0], trange[1], suffix='') + + return tvars diff --git a/pyspedas/elfin/mrma/__init__.py b/pyspedas/elfin/mrma/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyspedas/elfin/mrma/mrma.py b/pyspedas/elfin/mrma/mrma.py new file mode 100644 index 00000000..fda580de --- /dev/null +++ b/pyspedas/elfin/mrma/mrma.py @@ -0,0 +1,85 @@ +from .load import load + +def mrma(trange=['2020-11-5', '2020-11-6'], + probe='a', + datatype='mrma', + level='l1', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=False): + """ + This function loads data from the Magneto Resistive Magnetometer (MRMa) + + Parameters + ---------- + trange : list of str + time range of interest [starttime, endtime] with the format + 'YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + probe: str + Spacecraft identifier ('a' or 'b') + + datatype: str + Data type; Valid options: + 'mrma' for L1 data + + level: str + Data level; options: 'l1' (default: l1) + + suffix: str + The tplot variable names will be given this suffix. By default, + no suffix is added. + + get_support_data: bool + Data with an attribute "VAR_TYPE" with a value of "support_data" + will be loaded into tplot. By default, only loads in data with a + "VAR_TYPE" attribute of "data". + + varformat: str + The file variable formats to load into tplot. Wildcard character + "*" is accepted. By default, all variables are loaded in. + + varnames: list of str + List of variable names to load (if not specified, + all data variables are loaded) + + downloadonly: bool + Set this flag to download the CDF files, but not load them into + tplot variables + + notplot: bool + Return the data in hash tables instead of creating tplot variables + + no_update: bool + If set, only load data from your local cache + + time_clip: bool + Time clip the variables to exactly the range specified in the trange keyword + + Returns + ---------- + List of tplot variables created. + + """ + tvars = load(instrument='mrma', probe=probe, trange=trange, level=level, datatype=datatype, suffix=suffix, + get_support_data=get_support_data, varformat=varformat, varnames=varnames, downloadonly=downloadonly, + notplot=notplot, time_clip=time_clip, no_update=no_update) + + if tvars is None or notplot or downloadonly: + return tvars + + return mrma_postprocessing(tvars) + + +def mrma_postprocessing(variables): + """ + Placeholder for MRMa post-processing + """ + return variables + diff --git a/pyspedas/elfin/mrmi/__init__.py b/pyspedas/elfin/mrmi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyspedas/elfin/mrmi/mrmi.py b/pyspedas/elfin/mrmi/mrmi.py new file mode 100644 index 00000000..450e4121 --- /dev/null +++ b/pyspedas/elfin/mrmi/mrmi.py @@ -0,0 +1,78 @@ +from .load import load + +def mrmi(trange=['2020-11-5', '2020-11-6'], + probe='a', + datatype='mrmi', + level='l1', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=False): + """ + This function loads data from the Magneto Resistive Magnetometer (MRMi) + + Parameters + ---------- + trange : list of str + time range of interest [starttime, endtime] with the format + 'YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + probe: str + Spacecraft identifier ('a' or 'b') + + datatype: str + Data type; Valid options: + 'mrmi' for L1 data + + level: str + Data level; options: 'l1' (default: l1) + + suffix: str + The tplot variable names will be given this suffix. By default, + no suffix is added. + + get_support_data: bool + Data with an attribute "VAR_TYPE" with a value of "support_data" + will be loaded into tplot. By default, only loads in data with a + "VAR_TYPE" attribute of "data". + + varformat: str + The file variable formats to load into tplot. Wildcard character + "*" is accepted. By default, all variables are loaded in. + + varnames: list of str + List of variable names to load (if not specified, + all data variables are loaded) + + downloadonly: bool + Set this flag to download the CDF files, but not load them into + tplot variables + + notplot: bool + Return the data in hash tables instead of creating tplot variables + + no_update: bool + If set, only load data from your local cache + + time_clip: bool + Time clip the variables to exactly the range specified in the trange keyword + + Returns + ---------- + List of tplot variables created. + + """ + tvars = load(instrument='mrmi', probe=probe, trange=trange, level=level, datatype=datatype, suffix=suffix, + get_support_data=get_support_data, varformat=varformat, varnames=varnames, downloadonly=downloadonly, + notplot=notplot, time_clip=time_clip, no_update=no_update) + + if tvars is None or notplot or downloadonly: + return tvars + + return mrmi_postprocessing(tvars) + diff --git a/pyspedas/elfin/state/__init__.py b/pyspedas/elfin/state/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyspedas/elfin/state/state.py b/pyspedas/elfin/state/state.py new file mode 100644 index 00000000..a429894f --- /dev/null +++ b/pyspedas/elfin/state/state.py @@ -0,0 +1,86 @@ +from pyspedas.elfin.load import load + +def state(trange=['2020-11-5/10:00', '2020-11-5/12:00'], + probe='a', + datatype='defn', + level='l1', + suffix='', + get_support_data=False, + varformat=None, + varnames=[], + downloadonly=False, + notplot=False, + no_update=False, + time_clip=True): + """ + This function loads data from the State data (state) + + Parameters + ---------- + trange : list of str + time range of interest [starttime, endtime] with the format + 'YYYY-MM-DD','YYYY-MM-DD'] or to specify more or less than a day + ['YYYY-MM-DD/hh:mm:ss','YYYY-MM-DD/hh:mm:ss'] + + probe: str + Spacecraft identifier ('a' or 'b') + + datatype: str + Data type; Valid options: + 'defn' (default) + 'pred' + + level: str + Data level; options: 'l1' (default: l1) + + suffix: str + The tplot variable names will be given this suffix. By default, + no suffix is added. + + get_support_data: bool + Data with an attribute "VAR_TYPE" with a value of "support_data" + will be loaded into tplot. By default, only loads in data with a + "VAR_TYPE" attribute of "data". + + varformat: str + The file variable formats to load into tplot. Wildcard character + "*" is accepted. By default, all variables are loaded in. + + varnames: list of str + List of variable names to load (if not specified, + all data variables are loaded) + + downloadonly: bool + Set this flag to download the CDF files, but not load them into + tplot variables + + notplot: bool + Return the data in hash tables instead of creating tplot variables + + no_update: bool + If set, only load data from your local cache + + time_clip: bool + Time clip the variables to exactly the range specified in the trange keyword + + Returns + ---------- + List of tplot variables created. + + """ + tvars = load(instrument='state', probe=probe, trange=trange, level=level, datatype=datatype, suffix=suffix, + get_support_data=get_support_data, varformat=varformat, varnames=varnames, downloadonly=downloadonly, + notplot=notplot, time_clip=time_clip, no_update=no_update) + + if tvars is None or notplot or downloadonly: + return tvars + + return state_postprocessing(tvars) + + +def state_postprocessing(variables): + """ + Placeholder for State post-processing + """ + return variables + diff --git a/pyspedas/elfin/tests/__init__.py b/pyspedas/elfin/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyspedas/elfin/tests/test_epd_calibration.py b/pyspedas/elfin/tests/test_epd_calibration.py new file mode 100644 index 00000000..8a9f47dc --- /dev/null +++ b/pyspedas/elfin/tests/test_epd_calibration.py @@ -0,0 +1,70 @@ +import unittest +import importlib.resources + +from pyspedas.elfin.epd.calibration_l1 import read_epde_calibration_data + +EXPECTED_EPDE_CAL_DATA = [ + {'ch_efficiencies': [0.74, 0.8, 0.85, 0.86, 0.87, 0.87, 0.87, 0.87, 0.82, 0.8, 0.75, 0.6, 0.5, 0.45, 0.25, 0.05], + 'date': 1262390400.0, + 'ebins': [50.0, + 80.0, + 120.0, + 160.0, + 210.0, + 270.0, + 345.0, + 430.0, + 630.0, + 900.0, + 1300.0, + 1800.0, + 2500.0, + 3350.0, + 4150.0, + 5800.0], + 'gf': 0.15, + 'overaccumulation_factors': [1.0, 2.0, 3.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.15], + 'thresh_factors': [0.0325, + 0.208, + 0.156, + 0.13, + 0.13, + 0.13, + 0.13, + 0.13, + 0.13, + 0.13, + 0.13, + 0.13, + 0.13, + 0.13, + 0.13, + 0.13]}, + {'ch_efficiencies': [0.74, 0.8, 0.85, 0.86, 0.87, 0.87, 0.87, 0.87, 0.82, 0.8, 0.75, 0.6, 0.5, 0.45, 0.25, 0.05], + 'date': 1296705600.0, + 'ebins': [50.0, + 80.0, + 120.0, + 160.0, + 210.0, + 270.0, + 345.0, + 430.0, + 630.0, + 900.0, + 1300.0, + 1800.0, + 2500.0, + 3350.0, + 4150.0, + 5800.0], + 'gf': 3.14, + 'overaccumulation_factors': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.9, 1.0], + 'thresh_factors': [0.13, 0.208, 0.156, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13]} +] + +class EPDCalibrationTestCases(unittest.TestCase): + + def test_read_epde_calibration_data(self): + with importlib.resources.path("pyspedas.elfin.tests", "test_dataset/test_epde_cal_data.txt") as test_file_path: + self.assertListEqual(read_epde_calibration_data(test_file_path), EXPECTED_EPDE_CAL_DATA) \ No newline at end of file diff --git a/pyspedas/elfin/tests/test_epd_l1.py b/pyspedas/elfin/tests/test_epd_l1.py new file mode 100644 index 00000000..e7dfcd95 --- /dev/null +++ b/pyspedas/elfin/tests/test_epd_l1.py @@ -0,0 +1,137 @@ +""" +This module perform unitest on elfin epd l1 spectrogram by comparing +with tplot variable genrate by IDL routine + +How to run: + $ python -m pyspedas.elfin.tests.test_epd_l1 +""" +import unittest +import logging +import pytplot.get_data +from pytplot.importers.tplot_restore import tplot_restore +from numpy.testing import assert_allclose + +import pyspedas.elfin +from pyspedas.utilities.download import download +from pyspedas.elfin.config import CONFIG + +TEST_DATASET_PATH="test/" + +class TestELFL1Validation(unittest.TestCase): + """Tests of the data been identical to SPEDAS (IDL).""" + + @classmethod + def setUpClass(cls): + """ + IDL Data has to be downloaded to perform these tests + The IDL script that creates data file: epd_level1_check.pro + """ + # Testing time range + cls.t = ['2022-04-12/19:00:00','2022-04-12/19:15:00'] + cls.probe = 'b' + + # load epd l1 raw flux + calfile_name = f"{TEST_DATASET_PATH}validation_el{cls.probe}_epd_l1_raw_{cls.t[0][0:4]+cls.t[0][5:7]+cls.t[0][8:10]}.tplot" + calfile = download(remote_file=calfile_name, + remote_path=CONFIG['remote_data_dir'], + local_path=CONFIG['local_data_dir'], + no_download=False) + if not calfile: + raise unittest.SkipTest(f"Cannot download validation file {calfile_name}") + filename = CONFIG['local_data_dir'] + calfile_name + tplot_restore(filename) + cls.elf_pef_raw = pytplot.get_data(f"el{cls.probe}_pef_raw") + + # load epd l1 cps flux + calfile_name = f"{TEST_DATASET_PATH}validation_el{cls.probe}_epd_l1_cps_{cls.t[0][0:4]+cls.t[0][5:7]+cls.t[0][8:10]}.tplot" + calfile = download(remote_file=calfile_name, + remote_path=CONFIG['remote_data_dir'], + local_path=CONFIG['local_data_dir'], + no_download=False) + if not calfile: + raise unittest.SkipTest(f"Cannot download validation file {calfile_name}") + filename = CONFIG['local_data_dir'] + calfile_name + tplot_restore(filename) + cls.elf_pef_cps = pytplot.get_data(f"el{cls.probe}_pef_cps") + + # load epd l1 nflux flux + calfile_name = f"{TEST_DATASET_PATH}validation_el{cls.probe}_epd_l1_nflux_{cls.t[0][0:4]+cls.t[0][5:7]+cls.t[0][8:10]}.tplot" + calfile = download(remote_file=calfile_name, + remote_path=CONFIG['remote_data_dir'], + local_path=CONFIG['local_data_dir'], + no_download=False) + if not calfile: + raise unittest.SkipTest(f"Cannot download validation file {calfile_name}") + filename = CONFIG['local_data_dir'] + calfile_name + tplot_restore(filename) + cls.elf_pef_nflux = pytplot.get_data(f"el{cls.probe}_pef_nflux") + + # load epd l1 eflux spectrogram + calfile_name = f"{TEST_DATASET_PATH}validation_el{cls.probe}_epd_l1_eflux_{cls.t[0][0:4]+cls.t[0][5:7]+cls.t[0][8:10]}.tplot" + calfile = download(remote_file=calfile_name, + remote_path=CONFIG['remote_data_dir'], + local_path=CONFIG['local_data_dir'], + no_download=False) + if not calfile: + raise unittest.SkipTest(f"Cannot download validation file {calfile_name}") + filename = CONFIG['local_data_dir'] + calfile_name + tplot_restore(filename) + cls.elf_pef_eflux = pytplot.get_data(f"el{cls.probe}_pef_eflux") + cls.elf_pef_sectnum = pytplot.get_data(f"el{cls.probe}_pef_sectnum") + cls.elf_pef_nspinsinsum = pytplot.get_data(f"el{cls.probe}_pef_nspinsinsum") + cls.elf_pef_nsectors = pytplot.get_data(f"el{cls.probe}_pef_nsectors") + cls.elf_pef_spinper = pytplot.get_data(f"el{cls.probe}_pef_spinper") + + + def setUp(self): + """ We need to clean tplot variables before each run""" + pytplot.del_data('*') + + + def test_epd_l1_raw(self): + """Validate epd l1 raw spectogram""" + pyspedas.elfin.epd(trange=self.t, probe=self.probe, level='l1', type_='raw') + elf_pef_raw = pytplot.get_data(f"el{self.probe}_pef_raw") + assert_allclose(elf_pef_raw.y, self.elf_pef_raw.y, rtol=1e-01) + + logging.info("L1 RAW DATA TEST FINISHED.") + + + def test_epd_l1_cps(self): + """Validate epd l1 nflux spectogram""" + pyspedas.elfin.epd(trange=self.t, probe=self.probe, level='l1', type_='cps') + elf_pef_cps = pytplot.get_data(f"el{self.probe}_pef_cps") + assert_allclose(elf_pef_cps.y, self.elf_pef_cps.y, rtol=1e-01) + + logging.info("L1 CPS DATA TEST FINISHED.") + + + def test_epd_l1_nflux(self): + """Validate epd l1 nflux spectogram""" + pyspedas.elfin.epd(trange=self.t, probe=self.probe, level='l1', type_='nflux') + elf_pef_nflux = pytplot.get_data(f"el{self.probe}_pef_nflux") + assert_allclose(elf_pef_nflux.y, self.elf_pef_nflux.y, rtol=1e-01) + + logging.info("L1 NFLUX DATA TEST FINISHED.") + + + def test_epd_l1_eflux(self): + """Validate epd l1 elux spectogram""" + pyspedas.elfin.epd(trange=self.t, probe=self.probe, level='l1', type_='eflux') + elf_pef_eflux = pytplot.get_data(f"el{self.probe}_pef_eflux") + elf_pef_sectnum = pytplot.get_data(f"el{self.probe}_pef_sectnum") + elf_pef_nspinsinsum = pytplot.get_data(f"el{self.probe}_pef_nspinsinsum") + elf_pef_nsectors = pytplot.get_data(f"el{self.probe}_pef_nsectors") + elf_pef_spinper = pytplot.get_data(f"el{self.probe}_pef_spinper") + + assert_allclose(elf_pef_eflux.y, self.elf_pef_eflux.y, rtol=1) + assert_allclose(elf_pef_sectnum.y, self.elf_pef_sectnum.y, rtol=1e-02) + assert_allclose(elf_pef_nspinsinsum.y, self.elf_pef_nspinsinsum.y, rtol=1e-02) + assert_allclose(elf_pef_nsectors.y, self.elf_pef_nsectors.y, rtol=1e-02) + assert_allclose(elf_pef_spinper.y, self.elf_pef_spinper.y, rtol=1e-02) + + logging.info("L1 EFLUX DATA TEST FINISHED.") + + +if __name__ == '__main__': + unittest.main() diff --git a/pyspedas/elfin/tests/test_epd_l2.py b/pyspedas/elfin/tests/test_epd_l2.py new file mode 100644 index 00000000..f83dbcfa --- /dev/null +++ b/pyspedas/elfin/tests/test_epd_l2.py @@ -0,0 +1,306 @@ +""" +This module perform unitest on elfin epd l2 spectrogram by comparing +with tplot variable genrate by IDL routine + +How to run: + $ python -m pyspedas.elfin.tests.test_epd_l2 +""" +import unittest +import logging +from numpy.testing import assert_allclose +import pytplot.get_data +from pytplot.importers.tplot_restore import tplot_restore + +import pyspedas.elfin +from pyspedas.elfin.epd.calibration_l2 import spec_pa_sort +from pyspedas.utilities.download import download +from pyspedas.elfin.config import CONFIG + +TEST_DATASET_PATH="test/" + +class TestELFL2Validation(unittest.TestCase): + """Tests of the data been identical to SPEDAS (IDL).""" + + @classmethod + def setUpClass(cls): + """ + IDL Data has to be downloaded to perform these tests + The IDL script that creates data file: epd_level2_check2.pro + """ + + # Testing time range + #cls.t = ['2022-04-01/09:45:00','2022-04-01/10:10:00'] # elb with inner belt, pass + #cls.probe = 'b' + cls.t = ['2022-08-28/15:54','2022-08-28/16:15'] # pass + cls.probe = 'a' + + # load epd l2 hs nflux spectrogram + calfile_name = f"{TEST_DATASET_PATH}validation_el{cls.probe}_epd_l2_hs_nflux_{cls.t[0][0:4]+cls.t[0][5:7]+cls.t[0][8:10]}.tplot" + calfile = download(remote_file=calfile_name, + remote_path=CONFIG['remote_data_dir'], + local_path=CONFIG['local_data_dir'], + no_download=False) + if not calfile: + # Skip tests + raise unittest.SkipTest(f"Cannot download validation file {calfile_name}") + filename = CONFIG['local_data_dir'] + calfile_name + tplot_restore(filename) + cls.elf_pef_hs_nflux_ch0 = pytplot.get_data(f"el{cls.probe}_pef_hs_nflux_ch0") + cls.elf_pef_hs_nflux_ch1 = pytplot.get_data(f"el{cls.probe}_pef_hs_nflux_ch1") + cls.elf_pef_hs_nflux_ch2 = pytplot.get_data(f"el{cls.probe}_pef_hs_nflux_ch2") + cls.elf_pef_hs_nflux_ch3 = pytplot.get_data(f"el{cls.probe}_pef_hs_nflux_ch3") + cls.elf_pef_hs_nflux_omni = pytplot.get_data(f"el{cls.probe}_pef_hs_nflux_omni") + cls.elf_pef_hs_nflux_para = pytplot.get_data(f"el{cls.probe}_pef_hs_nflux_para") + cls.elf_pef_hs_nflux_anti = pytplot.get_data(f"el{cls.probe}_pef_hs_nflux_anti") + cls.elf_pef_hs_nflux_perp = pytplot.get_data(f"el{cls.probe}_pef_hs_nflux_perp") + cls.elf_pef_hs_antiLCdeg = pytplot.get_data(f"el{cls.probe}_pef_hs_antiLCdeg") + cls.elf_pef_hs_LCdeg = pytplot.get_data(f"el{cls.probe}_pef_hs_LCdeg") + cls.elf_pef_Et_nflux = pytplot.get_data(f"el{cls.probe}_pef_Et_nflux") + cls.elf_pef_pa = pytplot.get_data(f"el{cls.probe}_pef_pa") + cls.elf_pef_hs_Epat_nflux_ch0 = pytplot.get_data(f"el{cls.probe}_pef_hs_Epat_nflux_ch0") + # Epat is 3d, can't save it with idl + cls.elf_pef_hs_Epat_nflux_ch1 = pytplot.get_data(f"el{cls.probe}_pef_hs_Epat_nflux_ch1") + + # load epd l2 hs eflux spectrogram + calfile_name = f"{TEST_DATASET_PATH}validation_el{cls.probe}_epd_l2_hs_eflux_{cls.t[0][0:4]+cls.t[0][5:7]+cls.t[0][8:10]}.tplot" + calfile = download(remote_file=calfile_name, + remote_path=CONFIG['remote_data_dir'], + local_path=CONFIG['local_data_dir'], + no_download=False) + if not calfile: + raise unittest.SkipTest(f"Cannot download validation file {calfile_name}") + filename = CONFIG['local_data_dir'] + calfile_name + tplot_restore(filename) + cls.elf_pef_hs_eflux_ch0 = pytplot.get_data(f"el{cls.probe}_pef_hs_eflux_ch0") + cls.elf_pef_hs_eflux_ch1 = pytplot.get_data(f"el{cls.probe}_pef_hs_eflux_ch1") + cls.elf_pef_hs_eflux_ch2 = pytplot.get_data(f"el{cls.probe}_pef_hs_eflux_ch2") + cls.elf_pef_hs_eflux_ch3 = pytplot.get_data(f"el{cls.probe}_pef_hs_eflux_ch3") + cls.elf_pef_hs_eflux_omni = pytplot.get_data(f"el{cls.probe}_pef_hs_eflux_omni") + cls.elf_pef_hs_eflux_para = pytplot.get_data(f"el{cls.probe}_pef_hs_eflux_para") + cls.elf_pef_hs_eflux_anti = pytplot.get_data(f"el{cls.probe}_pef_hs_eflux_anti") + cls.elf_pef_hs_eflux_perp = pytplot.get_data(f"el{cls.probe}_pef_hs_eflux_perp") + cls.elf_pef_Et_eflux = pytplot.get_data(f"el{cls.probe}_pef_Et_eflux") + cls.elf_pef_hs_Epat_eflux_ch0 = pytplot.get_data(f"el{cls.probe}_pef_hs_Epat_eflux_ch0") + # Epat is 3d, can't save it with idl + cls.elf_pef_hs_Epat_eflux_ch1 = pytplot.get_data(f"el{cls.probe}_pef_hs_Epat_eflux_ch1") + + + # load epd l2 fs nflux spectrogram + calfile_name = f"{TEST_DATASET_PATH}validation_el{cls.probe}_epd_l2_fs_nflux_{cls.t[0][0:4]+cls.t[0][5:7]+cls.t[0][8:10]}.tplot" + calfile = download(remote_file=calfile_name, + remote_path=CONFIG['remote_data_dir'], + local_path=CONFIG['local_data_dir'], + no_download=False) + if not calfile: + raise unittest.SkipTest(f"Cannot download validation file {calfile_name}") + filename = CONFIG['local_data_dir'] + calfile_name + tplot_restore(filename) + cls.elf_pef_fs_nflux_ch0 = pytplot.get_data(f"el{cls.probe}_pef_fs_nflux_ch0") + cls.elf_pef_fs_nflux_ch1 = pytplot.get_data(f"el{cls.probe}_pef_fs_nflux_ch1") + cls.elf_pef_fs_nflux_omni = pytplot.get_data(f"el{cls.probe}_pef_fs_nflux_omni") + cls.elf_pef_fs_nflux_para = pytplot.get_data(f"el{cls.probe}_pef_fs_nflux_para") + cls.elf_pef_fs_nflux_anti = pytplot.get_data(f"el{cls.probe}_pef_fs_nflux_anti") + cls.elf_pef_fs_nflux_perp = pytplot.get_data(f"el{cls.probe}_pef_fs_nflux_perp") + cls.elf_pef_fs_antiLCdeg = pytplot.get_data(f"el{cls.probe}_pef_fs_antiLCdeg") + cls.elf_pef_fs_LCdeg = pytplot.get_data(f"el{cls.probe}_pef_fs_LCdeg") + cls.elf_pef_fs_Epat_nflux_ch0 = pytplot.get_data(f"el{cls.probe}_pef_fs_Epat_nflux_ch0") + # Epat is 3d, can't save it with idl + cls.elf_pef_fs_Epat_nflux_ch1 = pytplot.get_data(f"el{cls.probe}_pef_fs_Epat_nflux_ch1") + + + # load epd l2 fs eflux spectrogram + calfile_name = f"{TEST_DATASET_PATH}validation_el{cls.probe}_epd_l2_fs_eflux_{cls.t[0][0:4]+cls.t[0][5:7]+cls.t[0][8:10]}.tplot" + calfile = download(remote_file=calfile_name, + remote_path=CONFIG['remote_data_dir'], + local_path=CONFIG['local_data_dir'], + no_download=False) + if not calfile: + raise unittest.SkipTest(f"Cannot download validation file {calfile_name}") + filename = CONFIG['local_data_dir'] + calfile_name + tplot_restore(filename) + cls.elf_pef_fs_eflux_ch0 = pytplot.get_data(f"el{cls.probe}_pef_fs_eflux_ch0") + cls.elf_pef_fs_eflux_ch1 = pytplot.get_data(f"el{cls.probe}_pef_fs_eflux_ch1") + cls.elf_pef_fs_eflux_omni = pytplot.get_data(f"el{cls.probe}_pef_fs_eflux_omni") + cls.elf_pef_fs_eflux_para = pytplot.get_data(f"el{cls.probe}_pef_fs_eflux_para") + cls.elf_pef_fs_eflux_anti = pytplot.get_data(f"el{cls.probe}_pef_fs_eflux_anti") + cls.elf_pef_fs_eflux_perp = pytplot.get_data(f"el{cls.probe}_pef_fs_eflux_perp") + cls.elf_pef_fs_Epat_eflux_ch0 = pytplot.get_data(f"el{cls.probe}_pef_fs_Epat_eflux_ch0") + # Epat is 3d, can't save it with idl + cls.elf_pef_fs_Epat_eflux_ch1 = pytplot.get_data(f"el{cls.probe}_pef_fs_Epat_eflux_ch1") + + + def setUp(self): + """ We need to clean tplot variables before each run""" + pytplot.del_data('*') + + + def test_epd_l2_hs_nflux(self): + """Validate epd l2 halfspin nflux spectogram""" + pyspedas.elfin.epd(trange=self.t, probe=self.probe, level='l2',no_update=True) + elf_pef_hs_nflux_ch0 = pytplot.get_data(f"el{self.probe}_pef_hs_nflux_ch0") + elf_pef_hs_nflux_ch1 = pytplot.get_data(f"el{self.probe}_pef_hs_nflux_ch1") + elf_pef_hs_nflux_ch2 = pytplot.get_data(f"el{self.probe}_pef_hs_nflux_ch2") + elf_pef_hs_nflux_ch3 = pytplot.get_data(f"el{self.probe}_pef_hs_nflux_ch3") + elf_pef_hs_nflux_perp = pytplot.get_data(f"el{self.probe}_pef_hs_nflux_perp") + elf_pef_hs_nflux_anti = pytplot.get_data(f"el{self.probe}_pef_hs_nflux_anti") + elf_pef_hs_nflux_para = pytplot.get_data(f"el{self.probe}_pef_hs_nflux_para") + elf_pef_hs_nflux_omni = pytplot.get_data(f"el{self.probe}_pef_hs_nflux_omni") + elf_pef_hs_antiLCdeg = pytplot.get_data(f"el{self.probe}_pef_hs_antiLCdeg") + elf_pef_hs_LCdeg = pytplot.get_data(f"el{self.probe}_pef_hs_LCdeg") + elf_pef_hs_Epat_nflux = pytplot.get_data(f"el{self.probe}_pef_hs_Epat_nflux") + elf_pef_Et_nflux = pytplot.get_data(f"el{self.probe}_pef_Et_nflux") + elf_pef_pa = pytplot.get_data(f"el{self.probe}_pef_pa") + + assert_allclose(elf_pef_hs_Epat_nflux.v1, self.elf_pef_hs_Epat_nflux_ch1.v, rtol=1) + assert_allclose(elf_pef_hs_Epat_nflux.y[:,:,0], self.elf_pef_hs_Epat_nflux_ch0.y, rtol=1e-02) + assert_allclose(elf_pef_hs_Epat_nflux.y[:,:,1], self.elf_pef_hs_Epat_nflux_ch1.y, rtol=1e-02) + assert_allclose(elf_pef_hs_LCdeg.y, self.elf_pef_hs_LCdeg.y, rtol=1e-02) + assert_allclose(elf_pef_hs_antiLCdeg.y, self.elf_pef_hs_antiLCdeg.y, rtol=1e-02) + assert_allclose(elf_pef_pa.y, self.elf_pef_pa.y, rtol=1e-02) + assert_allclose(elf_pef_Et_nflux.y, self.elf_pef_Et_nflux.y, rtol=1e-02) + assert_allclose(elf_pef_hs_nflux_omni.y, self.elf_pef_hs_nflux_omni.y, rtol=1e-02) + assert_allclose(elf_pef_hs_nflux_para.y, self.elf_pef_hs_nflux_para.y, rtol=1e-02) + assert_allclose(elf_pef_hs_nflux_anti.y, self.elf_pef_hs_nflux_anti.y, rtol=1e-02) + assert_allclose(elf_pef_hs_nflux_perp.y, self.elf_pef_hs_nflux_perp.y, rtol=1e-02) + # test pa spectogram ch0 + spec2plot, _ = spec_pa_sort(self.elf_pef_hs_nflux_ch0.y, self.elf_pef_hs_nflux_ch0.v) + # idl variable use aceding and decending pa + assert_allclose(elf_pef_hs_nflux_ch0.y, spec2plot, rtol=1e-02) + # test pa spectogram ch1 + spec2plot, _ = spec_pa_sort(self.elf_pef_hs_nflux_ch1.y, self.elf_pef_hs_nflux_ch1.v) + assert_allclose(elf_pef_hs_nflux_ch1.y, spec2plot, rtol=1e-02) + # test pa spectogram ch2 + spec2plot, _ = spec_pa_sort(self.elf_pef_hs_nflux_ch2.y, self.elf_pef_hs_nflux_ch2.v) + assert_allclose(elf_pef_hs_nflux_ch2.y, spec2plot, rtol=1e-02) + # test pa spectogram ch3 + spec2plot, _ = spec_pa_sort(self.elf_pef_hs_nflux_ch3.y, self.elf_pef_hs_nflux_ch3.v) + assert_allclose(elf_pef_hs_nflux_ch3.y, spec2plot, rtol=1e-02) + + logging.info("HALFSPIN NFLUX DATA TEST FINISHED.") + + + def test_epd_l2_hs_eflux(self): + """Validate epd l2 halfspin eflux spectogram""" + pyspedas.elfin.epd( + trange=self.t, + probe=self.probe, + level='l2', + no_update=False, + type_='eflux', + Espec_LCfatol=40, + Espec_LCfptol=5,) + elf_pef_hs_eflux_ch0 = pytplot.get_data(f"el{self.probe}_pef_hs_eflux_ch0") + elf_pef_hs_eflux_ch1 = pytplot.get_data(f"el{self.probe}_pef_hs_eflux_ch1") + elf_pef_hs_eflux_ch2 = pytplot.get_data(f"el{self.probe}_pef_hs_eflux_ch2") + elf_pef_hs_eflux_ch3 = pytplot.get_data(f"el{self.probe}_pef_hs_eflux_ch3") + elf_pef_hs_eflux_perp = pytplot.get_data(f"el{self.probe}_pef_hs_eflux_perp") + elf_pef_hs_eflux_anti = pytplot.get_data(f"el{self.probe}_pef_hs_eflux_anti") + elf_pef_hs_eflux_para = pytplot.get_data(f"el{self.probe}_pef_hs_eflux_para") + elf_pef_hs_eflux_omni = pytplot.get_data(f"el{self.probe}_pef_hs_eflux_omni") + elf_pef_hs_Epat_eflux = pytplot.get_data(f"el{self.probe}_pef_hs_Epat_eflux") + elf_pef_Et_eflux = pytplot.get_data(f"el{self.probe}_pef_Et_eflux") + + assert_allclose(elf_pef_hs_Epat_eflux.v1, self.elf_pef_hs_Epat_eflux_ch1.v, rtol=1) + assert_allclose(elf_pef_hs_Epat_eflux.y[:,:,0], self.elf_pef_hs_Epat_eflux_ch0.y, rtol=1e-02) + assert_allclose(elf_pef_hs_Epat_eflux.y[:,:,1], self.elf_pef_hs_Epat_eflux_ch1.y, rtol=1e-02) + assert_allclose(elf_pef_Et_eflux.y, self.elf_pef_Et_eflux.y, rtol=1e-02) + assert_allclose(elf_pef_hs_eflux_omni.y, self.elf_pef_hs_eflux_omni.y, rtol=2e-02) + assert_allclose(elf_pef_hs_eflux_para.y, self.elf_pef_hs_eflux_para.y, rtol=2e-02) + assert_allclose(elf_pef_hs_eflux_anti.y, self.elf_pef_hs_eflux_anti.y, rtol=2e-02) + assert_allclose(elf_pef_hs_eflux_perp.y, self.elf_pef_hs_eflux_perp.y, rtol=2e-02) + # test pa spectogram ch0 + spec2plot, _ = spec_pa_sort(self.elf_pef_hs_eflux_ch0.y, self.elf_pef_hs_eflux_ch0.v) + # idl variable use aceding and decending pa + assert_allclose(elf_pef_hs_eflux_ch0.y, spec2plot, rtol=1e-02) + # test pa spectogram ch1 + spec2plot, _ = spec_pa_sort(self.elf_pef_hs_eflux_ch1.y, self.elf_pef_hs_eflux_ch1.v) + assert_allclose(elf_pef_hs_eflux_ch1.y, spec2plot, rtol=1e-02) + # test pa spectogram ch2 + spec2plot, _ = spec_pa_sort(self.elf_pef_hs_eflux_ch2.y, self.elf_pef_hs_eflux_ch2.v) + assert_allclose(elf_pef_hs_eflux_ch2.y, spec2plot, rtol=1e-02) + # test pa spectogram ch3 + spec2plot, _ = spec_pa_sort(self.elf_pef_hs_eflux_ch3.y, self.elf_pef_hs_eflux_ch3.v) + assert_allclose(elf_pef_hs_eflux_ch3.y, spec2plot, rtol=1e-02) + + logging.info("HALFSPIN EFLUX DATA TEST FINISHED.") + + + def test_epd_l2_fs_nflux(self): + """Validate epd l2 fullspin nflux spectogram""" + pyspedas.elfin.epd( + trange=self.t, + probe=self.probe, + level='l2', + no_update=False, + fullspin=True, + PAspec_energybins=[(0,3),(4,6)], + ) + elf_pef_fs_nflux_ch0 = pytplot.get_data(f"el{self.probe}_pef_fs_nflux_ch0") + elf_pef_fs_nflux_ch1 = pytplot.get_data(f"el{self.probe}_pef_fs_nflux_ch1") + elf_pef_fs_nflux_perp = pytplot.get_data(f"el{self.probe}_pef_fs_nflux_perp") + elf_pef_fs_nflux_anti = pytplot.get_data(f"el{self.probe}_pef_fs_nflux_anti") + elf_pef_fs_nflux_para = pytplot.get_data(f"el{self.probe}_pef_fs_nflux_para") + elf_pef_fs_nflux_omni = pytplot.get_data(f"el{self.probe}_pef_fs_nflux_omni") + elf_pef_fs_antiLCdeg = pytplot.get_data(f"el{self.probe}_pef_fs_antiLCdeg") + elf_pef_fs_LCdeg = pytplot.get_data(f"el{self.probe}_pef_fs_LCdeg") + elf_pef_fs_Epat_nflux = pytplot.get_data(f"el{self.probe}_pef_fs_Epat_nflux") + + assert_allclose(elf_pef_fs_Epat_nflux.v1, self.elf_pef_fs_Epat_nflux_ch1.v, rtol=1) + assert_allclose(elf_pef_fs_Epat_nflux.y[:,:,0], self.elf_pef_fs_Epat_nflux_ch0.y, rtol=1e-02) + assert_allclose(elf_pef_fs_Epat_nflux.y[:,:,1], self.elf_pef_fs_Epat_nflux_ch1.y, rtol=1e-02) + assert_allclose(elf_pef_fs_LCdeg.y, self.elf_pef_fs_LCdeg.y, rtol=1e-02) + assert_allclose(elf_pef_fs_antiLCdeg.y, self.elf_pef_fs_antiLCdeg.y, rtol=1e-02) + assert_allclose(elf_pef_fs_nflux_omni.y, self.elf_pef_fs_nflux_omni.y, rtol=1e-02) + assert_allclose(elf_pef_fs_nflux_para.y, self.elf_pef_fs_nflux_para.y, rtol=1e-02) + assert_allclose(elf_pef_fs_nflux_anti.y, self.elf_pef_fs_nflux_anti.y, rtol=1e-02) + assert_allclose(elf_pef_fs_nflux_perp.y, self.elf_pef_fs_nflux_perp.y, rtol=1e-02) + # test pa spectogram ch0 + spec2plot, _ = spec_pa_sort(self.elf_pef_fs_nflux_ch0.y, self.elf_pef_fs_nflux_ch0.v) + # idl variable use aceding and decending pa + assert_allclose(elf_pef_fs_nflux_ch0.y, spec2plot, rtol=1e-02) + # test pa spectogram ch1 + spec2plot, _ = spec_pa_sort(self.elf_pef_fs_nflux_ch1.y, self.elf_pef_fs_nflux_ch1.v) + assert_allclose(elf_pef_fs_nflux_ch1.y, spec2plot, rtol=1e-02) + + logging.info("FULLSPIN NFLUX DATA TEST FINISHED.") + + + def test_epd_l2_fs_eflux(self): + """Validate epd l2 fullspin eflux spectogram""" + pyspedas.elfin.epd( + trange=self.t, + probe=self.probe, + level='l2', + no_update=False, + fullspin=True, + type_='eflux', + PAspec_energies=[(50,250),(250,430)], + ) + elf_pef_fs_eflux_ch0 = pytplot.get_data(f"el{self.probe}_pef_fs_eflux_ch0") + elf_pef_fs_eflux_ch1 = pytplot.get_data(f"el{self.probe}_pef_fs_eflux_ch1") + elf_pef_fs_eflux_perp = pytplot.get_data(f"el{self.probe}_pef_fs_eflux_perp") + elf_pef_fs_eflux_anti = pytplot.get_data(f"el{self.probe}_pef_fs_eflux_anti") + elf_pef_fs_eflux_para = pytplot.get_data(f"el{self.probe}_pef_fs_eflux_para") + elf_pef_fs_eflux_omni = pytplot.get_data(f"el{self.probe}_pef_fs_eflux_omni") + elf_pef_fs_Epat_eflux = pytplot.get_data(f"el{self.probe}_pef_fs_Epat_eflux") + + assert_allclose(elf_pef_fs_Epat_eflux.v1, self.elf_pef_fs_Epat_eflux_ch1.v, rtol=1) + assert_allclose(elf_pef_fs_Epat_eflux.y[:,:,0], self.elf_pef_fs_Epat_eflux_ch0.y, rtol=1e-02) + assert_allclose(elf_pef_fs_Epat_eflux.y[:,:,1], self.elf_pef_fs_Epat_eflux_ch1.y, rtol=1e-02) + assert_allclose(elf_pef_fs_eflux_omni.y, self.elf_pef_fs_eflux_omni.y, rtol=1e-02) + assert_allclose(elf_pef_fs_eflux_para.y, self.elf_pef_fs_eflux_para.y, rtol=1e-02) + assert_allclose(elf_pef_fs_eflux_anti.y, self.elf_pef_fs_eflux_anti.y, rtol=1e-02) + assert_allclose(elf_pef_fs_eflux_perp.y, self.elf_pef_fs_eflux_perp.y, rtol=1e-02) + # test pa spectogram ch0 + spec2plot, _ = spec_pa_sort(self.elf_pef_fs_eflux_ch0.y, self.elf_pef_fs_eflux_ch0.v) + # idl variable use aceding and decending pa + assert_allclose(elf_pef_fs_eflux_ch0.y, spec2plot, rtol=1e-02) + # test pa spectogram ch1 + spec2plot, _ = spec_pa_sort(self.elf_pef_fs_eflux_ch1.y, self.elf_pef_fs_eflux_ch1.v) + assert_allclose(elf_pef_fs_eflux_ch1.y, spec2plot, rtol=1e-02) + + logging.info("FULLSPIN EFLUX DATA TEST FINISHED.") + + +if __name__ == '__main__': + unittest.main() diff --git a/pyspedas/elfin/tests/test_state.py b/pyspedas/elfin/tests/test_state.py new file mode 100644 index 00000000..968ad4c7 --- /dev/null +++ b/pyspedas/elfin/tests/test_state.py @@ -0,0 +1,82 @@ +""" +This module perform unitest on elfin state file by comparing +with tplot variable genrate by IDL routine + +How to run: + $ python -m pyspedas.elfin.tests.test_state +""" +import unittest +import logging +import pytplot.get_data +from pytplot.importers.tplot_restore import tplot_restore +from numpy.testing import assert_allclose, assert_array_almost_equal, assert_array_equal + +import pyspedas.elfin +from pyspedas.utilities.download import download +from pyspedas.elfin.config import CONFIG + +TEST_DATASET_PATH="test/" + +class TestELFStateValidation(unittest.TestCase): + """Tests of the data been identical to SPEDAS (IDL).""" + + @classmethod + def setUpClass(cls): + """ + IDL Data has to be downloaded to perform these tests + The IDL script that creates data file: (epd_state_validation.pro) + """ + # Testing time range + cls.t = ['2021-10-12/23:00:00','2021-10-12/23:10:00'] + cls.probe = 'b' + + # Load validation variables from the test file + calfile_name = f"{TEST_DATASET_PATH}validation_el{cls.probe}_state_{cls.t[0][0:4]+cls.t[0][5:7]+cls.t[0][8:10]}.tplot" + calfile = download(remote_file=calfile_name, + remote_path=CONFIG['remote_data_dir'], + local_path=CONFIG['local_data_dir'], + no_download=False) + if not calfile: + raise unittest.SkipTest(f"Cannot download validation file {calfile_name}") + filename = CONFIG['local_data_dir'] + calfile_name + tplot_restore(filename) + cls.elf_pos_gei = pytplot.get_data(f"el{cls.probe}_pos_gei") + cls.elf_vel_gei = pytplot.get_data(f"el{cls.probe}_vel_gei") + cls.elf_att_gei = pytplot.get_data(f"el{cls.probe}_att_gei") + cls.elf_att_solution = pytplot.get_data(f"el{cls.probe}_att_solution_date") + cls.elf_att_flag = pytplot.get_data(f"el{cls.probe}_att_flag") + cls.elf_att_spinper = pytplot.get_data(f"el{cls.probe}_att_spinper") + cls.elf_spin_orbnorm = pytplot.get_data(f"el{cls.probe}_spin_orbnorm_angle") + cls.elf_spin_sun = pytplot.get_data(f"el{cls.probe}_spin_sun_angle") + + + def setUp(self): + """ We need to clean tplot variables before each run""" + pytplot.del_data('*') + + def test_state(self): + """Validate state data.""" + pyspedas.elfin.state(trange=self.t, probe=self.probe) + elf_pos_gei = pytplot.get_data(f"el{self.probe}_pos_gei") + elf_vel_gei = pytplot.get_data(f"el{self.probe}_vel_gei") + elf_att_gei = pytplot.get_data(f"el{self.probe}_att_gei") + elf_att_solution = pytplot.get_data(f"el{self.probe}_att_solution_date") + elf_att_flag = pytplot.get_data(f"el{self.probe}_att_flag") + elf_att_spinper = pytplot.get_data(f"el{self.probe}_att_spinper") + elf_spin_orbnorm = pytplot.get_data(f"el{self.probe}_spin_orbnorm_angle") + elf_spin_sun = pytplot.get_data(f"el{self.probe}_spin_sun_angle") + + assert_array_almost_equal(elf_pos_gei.y, self.elf_pos_gei.y, decimal=4) + assert_array_almost_equal(elf_vel_gei.y, self.elf_vel_gei.y, decimal=4) + assert_array_almost_equal(elf_att_gei.y, self.elf_att_gei.y, decimal=2) + assert_allclose(elf_att_solution.times, self.elf_att_solution.y, rtol=1e-3) + assert_array_equal(elf_att_flag.y, self.elf_att_flag.y) + assert_allclose(elf_att_spinper.y, self.elf_att_spinper.y, rtol=1e-2) + assert_allclose(elf_spin_orbnorm.y, self.elf_spin_orbnorm.y, rtol=1e-2) + assert_allclose(elf_spin_sun.y, self.elf_spin_sun.y, rtol=1e-2) + + logging.info("STATE DATA TEST FINISHED.") + + +if __name__ == '__main__': + unittest.main() diff --git a/pyspedas/elfin/tests/tests.py b/pyspedas/elfin/tests/tests.py new file mode 100644 index 00000000..449ac6ce --- /dev/null +++ b/pyspedas/elfin/tests/tests.py @@ -0,0 +1,46 @@ +import os +import unittest +from pyspedas.utilities.data_exists import data_exists +import pyspedas + + +class LoadTestCases(unittest.TestCase): + + def test_load_fgm_data(self): + out_vars = pyspedas.elfin.fgm(time_clip=True) + self.assertTrue(data_exists('ela_fgs')) + + def test_load_epd_data(self): + out_vars = pyspedas.elfin.epd() + self.assertTrue(data_exists('ela_pef')) + + def test_load_mrma_data(self): + out_vars = pyspedas.elfin.mrma() + self.assertTrue(data_exists('ela_mrma')) + + def test_load_mrmi_data(self): + out_vars = pyspedas.elfin.mrmi() + self.assertTrue(data_exists('ela_mrmi')) + + def test_load_state_data(self): + out_vars = pyspedas.elfin.state() + self.assertTrue(data_exists('ela_pos_gei')) + self.assertTrue(data_exists('ela_vel_gei')) + + def test_load_eng_data(self): + out_vars = pyspedas.elfin.eng() + self.assertTrue(data_exists('ela_fc_idpu_temp')) + + def test_load_notplot(self): + out_vars = pyspedas.elfin.epd(notplot=True) + self.assertTrue('ela_pef' in out_vars) + + def test_downloadonly(self): + files = pyspedas.elfin.epd(downloadonly=True, trange=['2020-11-01', '2020-11-02']) + self.assertTrue(os.path.exists(files[0])) + + +if __name__ == '__main__': + unittest.main() + + \ No newline at end of file