Experiments

This is where you would create your experiment that you would like to run on the radar. The following are a couple of examples of current SuperDARN experiments, and a brief discussion of the update() method which will be implemented at a later date.

normalscan

Normalscan is a very common experiment for SuperDARN. It does not update itself, so no update() method is necessary. It only has a single slice, as there is only one frequency, pulse_len, beam_order, etc. Since there is only one slice there is no need for an interface dictionary.

normalsound.py
 1"""
 2    normalscan
 3    ~~~~~~~~~~
 4    Standard radar operating experiment. Transmits a single frequency signal.
 5
 6    :copyright: 2023 SuperDARN Canada
 7"""
 8
 9import borealis_experiments.superdarn_common_fields as scf
10from experiment_prototype.experiment_prototype import ExperimentPrototype
11
12
13class Normalscan(ExperimentPrototype):
14    def __init__(self, **kwargs):
15        """
16        kwargs:
17
18        freq: int
19
20        """
21        cpid = 151
22        super().__init__(cpid)
23
24        if scf.IS_FORWARD_RADAR:
25            beams_to_use = scf.STD_16_FORWARD_BEAM_ORDER
26        else:
27            beams_to_use = scf.STD_16_REVERSE_BEAM_ORDER
28
29        # default frequency set here
30        freq = kwargs.get("freq", scf.COMMON_MODE_FREQ_1)
31
32        self.add_slice({  # slice_id = 0, there is only one slice.
33            "pulse_sequence": scf.SEQUENCE_7P,
34            "tau_spacing": scf.TAU_SPACING_7P,
35            "pulse_len": scf.PULSE_LEN_45KM,
36            "num_ranges": scf.STD_NUM_RANGES,
37            "first_range": scf.STD_FIRST_RANGE,
38            "intt": scf.INTT_7P,  # duration of an integration, in ms
39            "beam_angle": scf.STD_16_BEAM_ANGLE,
40            "rx_beam_order": beams_to_use,
41            "tx_beam_order": beams_to_use,
42            "scanbound": scf.easy_scanbound(scf.INTT_7P, beams_to_use), #1 min scan
43            "freq" : freq, #kHz
44            "acf": True,
45            "xcf": True,  # cross-correlation processing
46            "acfint": True,  # interferometer acfs
47            "wait_for_first_scanbound": False,
48        })

twofsound

Twofsound is a common variant of the normalscan experiment for SuperDARN. It does not update itself, so no update() method is necessary. It has two frequencies so will require two slices. The frequencies switch after a full scan (full cycle through the beams), therefore the interfacing between slices 0 and 1 should be ‘SCAN’.

twofsound.py
 1#!/usr/bin/python
 2
 3"""
 4    twofsound
 5    ~~~~~~~~~
 6    Standard operating Borealis experiment. Alternates transmitting in two different frequencies.
 7
 8    :copyright: 2023 SuperDARN Canada
 9"""
10
11import copy
12
13from experiment_prototype.experiment_prototype import ExperimentPrototype
14import borealis_experiments.superdarn_common_fields as scf
15
16
17class Twofsound(ExperimentPrototype):
18
19    def __init__(self, **kwargs):
20        cpid = 3503
21
22        if scf.IS_FORWARD_RADAR:
23            beams_to_use = scf.STD_16_FORWARD_BEAM_ORDER
24        else:
25            beams_to_use = scf.STD_16_REVERSE_BEAM_ORDER
26
27        if scf.options.site_id in ["cly", "rkn", "inv"]:
28            num_ranges = scf.POLARDARN_NUM_RANGES
29        if scf.options.site_id in ["sas", "pgr", "lab"]:
30            num_ranges = scf.STD_NUM_RANGES
31
32        tx_freq_1 = scf.COMMON_MODE_FREQ_1
33        tx_freq_2 = scf.COMMON_MODE_FREQ_2
34
35        if kwargs:
36            if 'freq1' in kwargs.keys():
37                tx_freq_1 = int(kwargs['freq1'])
38
39            if 'freq2' in kwargs.keys():
40                tx_freq_2 = int(kwargs['freq2'])
41
42        rxctrfreq = txctrfreq = int((tx_freq_1 + tx_freq_2) / 2)
43
44        slice_1 = {  # slice_id = 0, the first slice
45            "pulse_sequence": scf.SEQUENCE_7P,
46            "tau_spacing": scf.TAU_SPACING_7P,
47            "pulse_len": scf.PULSE_LEN_45KM,
48            "num_ranges": num_ranges,
49            "first_range": scf.STD_FIRST_RANGE,
50            "intt": scf.INTT_7P,  # duration of an integration, in ms
51            "beam_angle": scf.STD_16_BEAM_ANGLE,
52            "rx_beam_order": beams_to_use,
53            "tx_beam_order": beams_to_use,
54            "scanbound": scf.easy_scanbound(scf.INTT_7P, beams_to_use),
55            "freq": tx_freq_1,     # kHz
56            "txctrfreq": txctrfreq,
57            "rxctrfreq": rxctrfreq,
58            "acf": True,
59            "xcf": True,  # cross-correlation processing
60            "acfint": True,  # interferometer acfs
61        }
62
63        slice_2 = copy.deepcopy(slice_1)
64        slice_2['freq'] = tx_freq_2
65
66        super().__init__(cpid, comment_string='Twofsound classic scan-by-scan')
67
68        self.add_slice(slice_1)
69
70        self.add_slice(slice_2, interfacing_dict={0: 'SCAN'})
71

full_fov

See Full FOV Imaging for more information.

full_fov.py
 1#!/usr/bin/python
 2
 3"""
 4    full_fov
 5    ~~~~~~~~
 6    The mode transmits with a pre-calculated phase progression across the array which illuminates
 7    the full FOV, and receives on all antennas. The first pulse in each sequence starts on the 0.1
 8    second boundaries, to enable bistatic listening on other radars.
 9
10    :copyright: 2022 SuperDARN Canada
11    :author: Remington Rohel
12"""
13import numpy as np
14
15from utils.signals import get_phase_shift
16import borealis_experiments.superdarn_common_fields as scf
17from experiment_prototype.experiment_prototype import ExperimentPrototype
18
19
20def rx_phase_pattern(beam_angle, freq_khz, antenna_locations):
21    # Chebyshev 30-dB window
22    window = [0.2910, 0.3173, 0.4557, 0.6018, 0.7424, 0.8637, 0.9528, 1.0000,
23              1.0000, 0.9528, 0.8637, 0.7424, 0.6018, 0.4557, 0.3173, 0.2910]
24
25    adjusted_rx_beam_directions = {
26        10400: [-25., -21.2, -18.3, -15.5, -11.4, -7.7, -5., -2.1,
27                2.1, 5., 7.7, 11.4, 15.5, 18.3, 21.2, 25.],
28        10500: [-24.8, -20.7, -17.9, -14.8, -11.8, -8.6, -4.9, -1.9,
29                1.9, 4.9, 8.6, 11.8, 14.8, 17.9, 20.7, 24.8],
30        10600: [-25., -20.7, -17.8, -14.9, -11.9, -8.5, -4.8, -1.9,
31                1.9, 4.8, 8.5, 11.9, 14.9, 17.8, 20.7, 25.],
32        10700: [-24.5, -21.4, -18.1, -15.3, -11.5, -7.7, -5.1, -2.1,
33                2.1, 5.1, 7.7, 11.5, 15.3, 18.1, 21.4, 24.5],
34        10800: [-25., -20.9, -17.8, -15.3, -11.6, -7.8, -4.8, -2.1,
35                2.1, 4.8, 7.8, 11.6, 15.3, 17.8, 20.9, 25.],
36        10900: [-24.9, -20.9, -17.7, -15.3, -11.7, -7.8, -4.7, -2.,
37                2., 4.7, 7.8, 11.7, 15.3, 17.7, 20.9, 25.],
38        12200: [-24.4, -21.5, -17.7, -14.2, -11.5, -8.2, -4.8, -1.8,
39                1.8, 4.8, 8.2, 11.5, 14.2, 17.7, 21.5, 24.4],
40        12300: [-24.2, -21.5, -17.5, -14.4, -11.5, -7.9, -5., -2.1,
41                2.1, 5., 7.9, 11.5, 14.4, 17.5, 21.5, 24.2],
42        12500: [-24.3, -21.4, -17.8, -14.1, -11.4, -8.2, -4.9, -1.8,
43                1.8, 4.9, 8.2, 11.4, 14.1, 17.8, 21.4, 24.3],
44        13000: [-23.9, -21.5, -18.4, -14.8, -11.4, -7.8, -4.6, -2.4,
45                2.4, 4.6, 7.8, 11.4, 14.8, 18.4, 21.5, 23.9],
46        13100: [-24.5, -21., -18.4, -13.7, -11.1, -8.5, -4.7, -1.5,
47                1.5, 4.7, 8.5, 11.1, 13.7, 18.4, 21., 24.5],
48        13200: [-24.8, -21.6, -18.4, -14., -11.7, -8.4, -4.6, -2.2,
49                2.2, 4.6, 8.4, 11.7, 14., 18.4, 21.6, 24.8],
50    }
51
52    shift = get_phase_shift(adjusted_rx_beam_directions[int(freq_khz)], freq_khz, antenna_locations[:, 0]) * 0.9999999
53
54    # Apply a window to the antenna data streams of the main array
55    if antenna_locations.shape[0] == 16:
56        shift = np.einsum('ij,j->ij', shift, np.array(window, dtype=np.float32))
57
58    return shift
59
60
61class FullFOV(ExperimentPrototype):
62    def __init__(self, **kwargs):
63        """
64        kwargs:
65
66        freq: int
67
68        """
69        cpid = 3800
70        super().__init__(cpid)
71
72        # default frequency set here
73        freq = scf.COMMON_MODE_FREQ_1
74
75        if kwargs:
76            if 'freq' in kwargs.keys():
77                freq = kwargs['freq']
78
79        print('Frequency set to {}'.format(freq))   # TODO: Log
80
81        self.add_slice({  # slice_id = 0, there is only one slice.
82            "pulse_sequence": scf.SEQUENCE_7P,
83            "tau_spacing": scf.TAU_SPACING_7P,
84            "pulse_len": scf.PULSE_LEN_45KM,
85            "num_ranges": scf.STD_NUM_RANGES,
86            "first_range": scf.STD_FIRST_RANGE,
87            "intt": scf.INTT_7P,  # duration of an integration, in ms
88            "beam_angle": scf.STD_16_BEAM_ANGLE,
89            "rx_beam_order": [[i for i in range(len(scf.STD_16_BEAM_ANGLE))]],
90            "tx_beam_order": [0],   # only one pattern
91            "tx_antenna_pattern": scf.easy_widebeam,
92            "rx_antenna_pattern": rx_phase_pattern,
93            "freq": freq,  # kHz
94            "acf": True,
95            "xcf": True,  # cross-correlation processing
96            "acfint": True,  # interferometer acfs
97        })

bistatic_test

See Bistatic Experiments for more information.

bistatic_test.py
  1#!/usr/bin/python
  2
  3"""
  4    bistatic_test
  5    ~~~~~~~~~~~~~
  6    The mode transmits with a pre-calculated phase progression across the array which illuminates
  7    the full FOV, and receives on all antennas. The first pulse in each sequence starts on the 0.1
  8    second boundaries, to enable bistatic listening on other radars. This mode also chooses a
  9    frequency from another radar to listen in on, also across the entire FOV simultaneously.
 10
 11    :copyright: 2022 SuperDARN Canada
 12    :author: Remington Rohel
 13"""
 14
 15import borealis_experiments.superdarn_common_fields as scf
 16from experiment_prototype.experiment_utils import decimation_scheme as dm
 17from experiment_prototype.experiment_prototype import ExperimentPrototype
 18
 19
 20def two_stage_filter():
 21    """
 22    Two-stage kaiser window scheme.
 23
 24    Works well with the following parameters:
 25    sample_rate = 5e6
 26    dm_rate = [30, 50]
 27    transition_width = [150e3, 25e3]
 28    cutoff_hz = [10e3, 5e3]
 29    ripple_db = [115, 50]
 30    """
 31    sample_rate = 5e6  # 5 MHz
 32    dm_rate = [30, 50]  # downsampling rates after filters
 33    transition_width = [150e3, 30e3]  # transition from passband to stopband
 34    cutoff_hz = [10e3, 5e3]  # bandwidth for output of filter
 35    ripple_db = [115, 50]  # dB between passband and stopband
 36    scaling_factors = [1000.0, 10000.0]  # multiplicative factors for each filter stage
 37
 38    dm_rate_so_far = 1
 39    stages = []
 40    for i in range(2):
 41        rate = sample_rate / dm_rate_so_far
 42        taps = scaling_factors[i] * dm.create_firwin_filter_by_attenuation(
 43            rate, transition_width[i], cutoff_hz[i], ripple_db[i]
 44        )
 45        stages.append(dm.DecimationStage(i, rate, dm_rate[i], taps.tolist()))
 46        dm_rate_so_far *= dm_rate[i]
 47
 48    scheme = dm.DecimationScheme(sample_rate, sample_rate / dm_rate_so_far, stages=stages)
 49
 50    return scheme
 51
 52
 53class BistaticTest(ExperimentPrototype):
 54    """
 55    This experiment has different behaviour depending on the site that 
 56    is operating it. SAS, INV, and CLY operate normally (i.e. monostatically),
 57    while RKN and PGR 'listen in' on CLY, therefore operating as separate
 58    bistatic systems with CLY. All sites run a widebeam mode that 
 59    receives (and transmits for some sites) the entire FOV simultaneously.
 60    """
 61    def __init__(self, **kwargs):
 62        """
 63        kwargs:
 64            listen_to: str, one of the three-letter site codes. e.g. listen_to='cly'
 65            beam_order: str, beam order for tx. Only used if listen_to not specified. Format as '1,3,5,6-10',
 66                which will use beams [1, 3, 5, 6, 7, 8, 9, 10]
 67        """
 68        cpid = 3820
 69
 70        common_freqs = {            # copied from superdarn_common_fields.py - March 2025
 71            'sas': [10800, 13000],
 72            'pgr': [10900, 13100],
 73            'rkn': [10600, 12300],
 74            'inv': [10500, 12200],
 75            'cly': [10700, 12500]
 76        }
 77
 78        # default frequency set here
 79        listen_to = kwargs.get('listen_to', scf.options.site_id)   # If 'listen_to' specified, tune in to that radar
 80        if listen_to not in common_freqs.keys():
 81            raise ValueError('Not a valid site ID: {}'.format(listen_to))
 82
 83        freq = common_freqs.get(listen_to)[0]
 84
 85        slice_0 = {
 86            "pulse_sequence": scf.SEQUENCE_7P,
 87            "tau_spacing": scf.TAU_SPACING_7P,
 88            "pulse_len": scf.PULSE_LEN_45KM,
 89            "num_ranges": scf.STD_NUM_RANGES,
 90            "first_range": scf.STD_FIRST_RANGE,
 91            "intt": scf.INTT_7P,  # duration of an integration, in ms
 92            "beam_angle": scf.STD_16_BEAM_ANGLE,
 93            "freq": freq,  # kHz
 94            "scanbound": [i * 3.7 for i in range(len(scf.STD_16_BEAM_ANGLE))],  # align each aveperiod to 3.7s boundary
 95            "wait_for_first_scanbound": False,
 96            "decimation_scheme": two_stage_filter(),
 97            "align_sequences": True,     # align start of sequence to tenths of a second
 98        }
 99
100        if 'listen_to' in kwargs.keys() and 'beam_order' in kwargs.keys():  # Mutually exclusive arguments
101            raise ValueError('ERROR: Cannot specify both "listen_to" and "beam_order".')
102
103        if 'listen_to' not in kwargs.keys():  # Not listening to another radar, so must specify tx characteristics
104            # beam_order set here
105            if 'beam_order' in kwargs.keys():
106                tx_beam_order = []
107                beams = kwargs['beam_order'].split(',')
108                for beam in beams:
109                    # If a range was specified, include all numbers in that range (including endpoints)
110                    if '-' in beam:
111                        first_beam, last_beam = beam.split('-')
112                        tx_beam_order.extend(range(int(first_beam), int(last_beam) + 1))
113                    else:
114                        tx_beam_order.append(int(beam))
115                comment_str = 'Special tx beam order'
116            else:
117                tx_beam_order = [0]
118                slice_0['tx_antenna_pattern'] = scf.easy_widebeam
119                comment_str = 'Widebeam transmission'
120
121            slice_0['tx_beam_order'] = tx_beam_order
122            rx_beam_order = [[i for i in range(len(scf.STD_16_BEAM_ANGLE))]] * len(tx_beam_order)
123            slice_0['rx_beam_order'] = rx_beam_order    # Must have same first dimension as tx_beam_order
124
125        elif listen_to == scf.options.site_id:
126            slice_0['rx_beam_order'] = [[i for i in range(len(scf.STD_16_BEAM_ANGLE))]]
127            print('Defaulting to rx_only mode, "listen_to" set to this radar')
128            comment_str = 'Widebeam listening mode'
129
130        else:
131            slice_0['rx_beam_order'] = [[i for i in range(len(scf.STD_16_BEAM_ANGLE))]]
132            comment_str = 'Bistatic widebeam mode - listening to {}'.format(listen_to)
133
134        super().__init__(cpid, comment_string=comment_str)
135
136        self.add_slice(slice_0)
137