diff --git a/docs/source/building_an_experiment.rst b/docs/source/building_an_experiment.rst index ef912fd3..9e8b6744 100644 --- a/docs/source/building_an_experiment.rst +++ b/docs/source/building_an_experiment.rst @@ -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 diff --git a/docs/source/new_experiments.rst b/docs/source/new_experiments.rst index 0fbfe7d3..0f6c795c 100644 --- a/docs/source/new_experiments.rst +++ b/docs/source/new_experiments.rst @@ -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: -------------------- diff --git a/src/experiment_prototype/experiment_slice.py b/src/experiment_prototype/experiment_slice.py index 2c3947b1..2f1e5d23 100644 --- a/src/experiment_prototype/experiment_slice.py +++ b/src/experiment_prototype/experiment_slice.py @@ -60,6 +60,7 @@ "rx_int_antennas", "rx_main_antennas", "rxonly", + "rx_antenna_pattern", "scanbound", "seqoffset", "slice_id", @@ -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. @@ -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) @@ -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: diff --git a/src/experiment_prototype/scan_classes/sequences.py b/src/experiment_prototype/scan_classes/sequences.py index b5dcfe97..c56a791f 100644 --- a/src/experiment_prototype/scan_classes/sequences.py +++ b/src/experiment_prototype/scan_classes/sequences.py @@ -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}