Skip to content

Commit

Permalink
Merge pull request #432 from SuperDARNCanada/rx_antenna_pattern
Browse files Browse the repository at this point in the history
Rx antenna pattern addition should work now. Pull request here is related to SuperDARNCanada/borealis_experiments#11 (PR 10 was shifted to PR 11 to rebase from slice_refactor
  • Loading branch information
Doreban authored Jan 31, 2024
2 parents 6c77ab9 + 2278eff commit b5cdf67
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 4 deletions.
6 changes: 6 additions & 0 deletions docs/source/building_an_experiment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,12 @@ rx_int_antennas *defaults*
rx_main_antennas *defaults*
The antennas to receive on in main array, default is all antennas given max number from config.

rx_antenna_pattern *defaults*
Experiment-defined function which returns a complex weighting factor of magnitude <= 1 for each
beam direction scanned in the experiment. The return value of the function must be an array of
size [beam_angle, antenna_num]. This function allows for custom beamforming of the receive
antennas for borealis processing of antenna iq to rawacf.

scanbound *defaults*
A list of seconds past the minute for averaging periods in a scan to align to. Defaults to None,
not required. If you set this, you will want to ensure that there is a slightly larger amount of
Expand Down
16 changes: 16 additions & 0 deletions docs/source/new_experiments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ the first dimension of the returned array matches the first dimension of ``rx_be
slice dictionary, and that ``num_main_antennas`` matches the number of main antennas in the config
file.

Custom beamforming of the results measured during a full field of view experiment is possible
through defining the ``rx_antenna_pattern`` field in the full field of view
experiment. A custom function can be written in the experiment and passed to borealis ::

beamforming_function(beam_angle, freq, antenna_count, antenna_spacing, offset=0.0):

...

slice_dict['tx_antenna_pattern'] = beamforming_function

The function should expect to receive beam angles, operating frequencies, number of antennas,
antenna spacing, and an offset. This function will be called for both the rx signals from the main
array and the interferometer array. The return is expected to be the desired phase for beamforming
each antenna, and should be of size [beam_angle, antenna_count]. The magnitude of each entry should
be less than or equal to 1.

.. _bistatic experiments:

--------------------
Expand Down
37 changes: 37 additions & 0 deletions src/experiment_prototype/experiment_slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"rx_int_antennas",
"rx_main_antennas",
"rxonly",
"rx_antenna_pattern",
"scanbound",
"seqoffset",
"slice_id",
Expand Down Expand Up @@ -201,6 +202,11 @@ class ExperimentSlice:
from config.
rx_main_antennas *defaults*
The antennas to receive on in main array, default is all antennas given max number from config.
rx_antenna_pattern *defaults*
Experiment-defined function which returns a complex weighting factor of magnitude <= 1 for each
beam direction scanned in the experiment. The return value of the function must be an array of
size [beam_angle, antenna_num]. This function allows for custom beamforming of the receive
antennas for borealis processing of antenna iq to rawacf.
scanbound *defaults*
A list of seconds past the minute for averaging periods in a scan to align to. Defaults to None,
not required. If one slice in an experiment has a scanbound, they all must.
Expand Down Expand Up @@ -297,6 +303,7 @@ class ExperimentSlice:
max_items=options.intf_antenna_count,
unique_items=True)] = Field(default_factory=list)
tx_antenna_pattern: Optional[Callable] = default_callable
rx_antenna_pattern: Optional[Callable] = default_callable
tx_beam_order: Optional[beam_order_type] = Field(default_factory=list)
intt: Optional[confloat(ge=0)] = None
scanbound: Optional[list[confloat(ge=0)]] = Field(default_factory=list)
Expand Down Expand Up @@ -438,6 +445,36 @@ def check_tx_antenna_pattern(cls, tx_antenna_pattern, values):
f"values with a magnitude greater than 1")
return tx_antenna_pattern

@validator('rx_antenna_pattern')
def check_rx_antenna_pattern(cls, rx_antenna_pattern, values):
if rx_antenna_pattern is default_callable: # No value given
return

# Main and interferometer patterns
antenna_pattern = [rx_antenna_pattern(values['beam_angle'], values['freq'], options.main_antenna_count,
options.main_antenna_spacing),
rx_antenna_pattern(values['beam_angle'], values['freq'], options.intf_antenna_count,
options.intf_antenna_spacing, offset=-100)]
for index in range(0, len(antenna_pattern)):
if index == 0:
pattern = "main"
antenna_num = options.main_antenna_count
else:
pattern = "interferometer"
antenna_num = options.intf_antenna_count
if not isinstance(antenna_pattern[index], np.ndarray):
raise ValueError(f"Slice {values['slice_id']} {pattern} array rx antenna pattern return is "
f"not a numpy array")
else:
if antenna_pattern[index].shape != (len(values['beam_angle']), antenna_num):
raise ValueError(f"Slice {values['slice_id']} {pattern} array must be the same shape as"
f" ([beam angle], [antenna_count])")
antenna_pattern_mag = np.abs(antenna_pattern[index])
if np.argwhere(antenna_pattern_mag > 1.0).size > 0:
raise ValueError(f"Slice {values['slice_id']} {pattern} array rx antenna pattern return must not have "
f"any values with a magnitude greater than 1")
return rx_antenna_pattern

@validator('rx_beam_order', each_item=True)
def check_rx_beam_order(cls, rx_beam, values):
if 'beam_angle' in values:
Expand Down
15 changes: 11 additions & 4 deletions src/experiment_prototype/scan_classes/sequences.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,17 @@ def __init__(self, seqn_keys, sequence_slice_dict, sequence_interface, transmit_


# Now we set up the phases for receive side
rx_main_phase_shift = get_phase_shift(exp_slice.beam_angle, freq_khz, main_antenna_count,
main_antenna_spacing)
rx_intf_phase_shift = get_phase_shift(exp_slice.beam_angle, freq_khz, intf_antenna_count,
intf_antenna_spacing, intf_offset[0])
if exp_slice.rx_antenna_pattern is not None:
# Returns an array of size [beam_angle] of complex numbers of magnitude <= 1
rx_main_phase_shift = exp_slice.rx_antenna_pattern(exp_slice.beam_angle, freq_khz, main_antenna_count,
main_antenna_spacing)
rx_intf_phase_shift = exp_slice.rx_antenna_pattern(exp_slice.beam_angle, freq_khz, intf_antenna_count,
intf_antenna_spacing, intf_offset[0])
else:
rx_main_phase_shift = get_phase_shift(exp_slice.beam_angle, freq_khz, main_antenna_count,
main_antenna_spacing)
rx_intf_phase_shift = get_phase_shift(exp_slice.beam_angle, freq_khz, intf_antenna_count,
intf_antenna_spacing, intf_offset[0])

self.rx_beam_phases[slice_id] = {'main': rx_main_phase_shift, 'intf': rx_intf_phase_shift}

Expand Down

0 comments on commit b5cdf67

Please sign in to comment.