diff --git a/python/packages/nisar/antenna/pattern.py b/python/packages/nisar/antenna/pattern.py index e338da570..d01be31db 100644 --- a/python/packages/nisar/antenna/pattern.py +++ b/python/packages/nisar/antenna/pattern.py @@ -1,4 +1,3 @@ -from warnings import warn from collections import defaultdict from isce3.core import Orbit, Attitude, Linspace from isce3.geometry import DEMInterpolator @@ -11,7 +10,8 @@ from nisar.antenna import TxTrmInfo, RxTrmInfo, TxBMF, RxDBF from nisar.antenna.beamformer import get_pulse_index from nisar.antenna.rx_channel_imbalance_helpers import ( - compute_all_rx_channel_imbalances_from_l0b + compute_all_rx_channel_imbalances_from_l0b, + _is_product_from_second_band ) import numpy as np @@ -196,6 +196,16 @@ def __init__(self, raw: Raw, dem: DEMInterpolator, self.freq_band = freq_band # get all polarization for a frequency band self.txrx_pols = raw.polarizations[self.freq_band] + # check if it is second band in only split spectrum scenario + is_second_band = True + for p in self.txrx_pols: + is_second_band &= _is_product_from_second_band( + raw, freq_band=freq_band, txrx_pol=p) + self._is_second_band = is_second_band + log.info( + f'Whether frequency band "{freq_band}" is the second band of ' + f'SSP -> {self._is_second_band}' + ) # comput all RX channel imbalances over all # txrx pols of a desired frequency band. # This RX imbalanced is basically LNA/CALTONE ratio. @@ -276,7 +286,7 @@ def __init__(self, raw: Raw, dem: DEMInterpolator, # fetch RX channel adjustment complex factors from # instrument file per RX pol. self.channel_adj_fact_rx[rx_p] = ins.channel_adjustment_factors_rx( - rx_p) + rx_p, is_second_band=self._is_second_band) # get rx el-cut patterns self.el_pat_rx[rx_p] = ant.el_cut_all(rx_p) @@ -341,7 +351,8 @@ def __init__(self, raw: Raw, dem: DEMInterpolator, # fetch TX channel adjustment complex factors from # instrument file per TX linear pol. self.channel_adj_fact_tx[tx_lp] = ( - ins.channel_adjustment_factors_tx(tx_lp) + ins.channel_adjustment_factors_tx( + tx_lp, is_second_band=self._is_second_band) ) # get tx el-cut patterns diff --git a/python/packages/nisar/products/readers/instrument/instrument_parser.py b/python/packages/nisar/products/readers/instrument/instrument_parser.py index a26eaa6e4..9ac535393 100644 --- a/python/packages/nisar/products/readers/instrument/instrument_parser.py +++ b/python/packages/nisar/products/readers/instrument/instrument_parser.py @@ -273,7 +273,7 @@ def get_filenames_dbf(self): return fn_ta, fn_ac - def channel_adjustment_factors_tx(self, pol): + def channel_adjustment_factors_tx(self, pol, is_second_band=False): """ Get channel adjustment complex factors for all TX channels of a polarization. @@ -286,6 +286,12 @@ def channel_adjustment_factors_tx(self, pol): pol : {'H', 'V'} Tx polarization. Must be a valid polarization in the instrument file. + is_second_band : bool, default=False + If True, it will get adjustment factors for the second + frequency band ("B"). + If the respective dataset does not exist, it will use + the original dataset for both bands (backward compatibility) + while raising a warning "MissingInstrumentFieldWarning". Returns ------- @@ -305,11 +311,14 @@ def channel_adjustment_factors_tx(self, pol): None if the field "channelAdjustment" does not exist in the instrument file. That is, no need for the TX channels adjustment in TX antenna pattern formation. + A new dataset "amplitudeB" is introduced to cover the second band + (typically called "B" for split spectrum case). This major change is + introduced in v3.0 """ - return self._channel_adjustment_factors('tx', pol) + return self._channel_adjustment_factors('tx', pol, is_second_band) - def channel_adjustment_factors_rx(self, pol): + def channel_adjustment_factors_rx(self, pol, is_second_band=False): """ Get channel adjustment complex factors for all RX channels of a polarization. @@ -323,6 +332,12 @@ def channel_adjustment_factors_rx(self, pol): pol : {'H', 'V'} Rx polarization. Must be a valid polarization in the instrument file. + is_second_band : bool, default=False + If True, it will get adjustment factors for the second + frequency band ("B"). + If the respective dataset does not exist, it will use + the original dataset for both bands (backward compatibility) + while raising a warning "MissingInstrumentFieldWarning". Returns ------- @@ -342,11 +357,14 @@ def channel_adjustment_factors_rx(self, pol): None if the field "channelAdjustment" does not exist in the instrument file. That is, no need for the RX channels adjustment in RX antenna pattern formation. + A new dataset "amplitudeB" is introduced to cover the second band + (typically called "B" for split spectrum case). This major change is + introduced in v3.0. """ - return self._channel_adjustment_factors('rx', pol) + return self._channel_adjustment_factors('rx', pol, is_second_band) - def _channel_adjustment_factors(self, side, pol): + def _channel_adjustment_factors(self, side, pol, is_second_band): """ Helper function for obtaining TX or RX channel adjustment factors. @@ -356,6 +374,12 @@ def _channel_adjustment_factors(self, side, pol): pol : {'H', 'V'} Tx or Rx polarization depending on `side`. Must be a valid polarization in the instrument file. + is_second_band : bool, default=False + If True, it will get adjustment factors for the second + frequency band ("B"). + If the respective dataset does not exist, it will use + the original dataset for both bands (backward compatibility) + while raising a warning "MissingInstrumentFieldWarning". Returns ------- @@ -375,6 +399,9 @@ def _channel_adjustment_factors(self, side, pol): None if the field "channelAdjustment" does not exist in the instrument file. That is, no need for the TX/RX channels adjustment in TX/RX antenna pattern formation. + A new dataset "amplitudeB" is introduced to cover the second band + (typically called "B" for split spectrum case). This major change is + introduced in v3.0. """ if pol not in self.pols: @@ -391,7 +418,19 @@ def _channel_adjustment_factors(self, side, pol): stacklevel=2) return None - else: + else: # the channel adjustment group exists + if is_second_band: + try: + ds = grp[f'{side}/amplitudeB'] + except KeyError: + warnings.warn( + f'{grp_name}{side}/amplitudeB', + category=MissingInstrumentFieldWarning, + stacklevel=2) + # revert to the original dataset + # used for all bands! + ds = grp[f'{side}/amplitude'] + return ds[()] return grp[f'{side}/amplitude'][()] def get_crosstalk(self): diff --git a/tests/data/README.md b/tests/data/README.md index e5bcc2a40..5196f224b 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -423,6 +423,15 @@ Note that a new instrument HDF5 sample file "REE_INSTRUMENT_TABLE_V2P0.h5" of ve The total number of range lines is *70*. The total number of range bins is *28927*. The Tx range lines types are of HPA, LNA, and BYPASS. BYPASS range line interval is *20*. +- **REE_INSTRUMENT_TABLE_V3P0.h5** + + The augmented version, *v3.0*, of the instrument HDF5 product "REE_INSTRUMENT_TABLE_V2P0.h5" + whose new spec are introduced on *06/21/2026*. + This file contains extra dataset "amplitudeB" for all polarizations covering both TX and RX + side. E.g, */HPOL/channelAdjustment/tx/amplitudeB*. + This new field supports instrument-related frequency-dependent adjustemnt factors for + the second frequency band (aka, "B") if available in split spectrum case. + ## Geoid EGM96 diff --git a/tests/data/bf/REE_INSTRUMENT_TABLE_V3P0.h5 b/tests/data/bf/REE_INSTRUMENT_TABLE_V3P0.h5 new file mode 100644 index 000000000..33966526e Binary files /dev/null and b/tests/data/bf/REE_INSTRUMENT_TABLE_V3P0.h5 differ diff --git a/tests/python/packages/nisar/products/readers/instrument_parser.py b/tests/python/packages/nisar/products/readers/instrument_parser.py index f58a3daae..b5c58f2ac 100644 --- a/tests/python/packages/nisar/products/readers/instrument_parser.py +++ b/tests/python/packages/nisar/products/readers/instrument_parser.py @@ -151,3 +151,82 @@ def test_instrument_parser_v2p0(): npt.assert_allclose(rx_adj_fact, adj_factors, err_msg='Wrong RX channel adjustment factors' f' for "{pol}"') + # check for band=B which does not exist to make sure it + # is handled properly for backward compatibility + tx_adj_fact = ins.channel_adjustment_factors_tx( + pol, is_second_band=True) + npt.assert_allclose(tx_adj_fact, adj_factors, + err_msg=( + 'Wrong TX channel adjustment factors ' + f'of second band for "{pol}"' + )) + rx_adj_fact = ins.channel_adjustment_factors_rx( + pol, is_second_band=True) + npt.assert_allclose(rx_adj_fact, adj_factors, + err_msg=( + 'Wrong RX channel adjustment factors ' + f'of second band for "{pol}"' + )) + + +def test_instrument_parser_v3p0(): + """ + Parsing v3.0 of NISAR instrument table where new dataset + "amplitudeB" is added to v2.0 to support the TX/RX channel + adjustment factors on the second frequency band used in only + split spectrum case! + + """ + # sub directory for test files under "isce3/tests/data" + sub_dir = 'bf' + + # HDF5 filename for instrument under "sub_dir" + instrument_file = 'REE_INSTRUMENT_TABLE_V3P0.h5' + + # channel adjustment factors stored in the file + def _powdb_phasedeg_to_complex(pow_db: float, phs_deg: float) -> float: + """Convert (power, phase) (dB, deg) to a complex amplitude""" + return 10 ** (pow_db / 20) * np.exp(1j * np.deg2rad(phs_deg)) + + adj_fact_tx = {} + adj_fact_tx['H'] = np.full( + shape=(12,), + fill_value=_powdb_phasedeg_to_complex(0.1, 10), + dtype='c16' + ) + adj_fact_tx['V'] = np.full( + shape=(12,), + fill_value=_powdb_phasedeg_to_complex(0.2, 20), + dtype='c16' + ) + + adj_fact_rx = {} + adj_fact_rx['H'] = np.full( + shape=(12,), + fill_value=_powdb_phasedeg_to_complex(-0.1, -10), + dtype='c16' + ) + adj_fact_rx['V'] = np.full( + shape=(12,), + fill_value=_powdb_phasedeg_to_complex(-0.2, -20), + dtype='c16' + ) + + # construct the parser + with InstrumentParser(os.path.join(iscetest.data, sub_dir, + instrument_file)) as ins: + for pol in ('H', 'V'): + tx_adj_fact = ins.channel_adjustment_factors_tx( + pol, is_second_band=True) + npt.assert_allclose(tx_adj_fact, adj_fact_tx[pol], + err_msg=( + 'Wrong TX channel adjustment factors ' + f'of second band for "{pol}"' + )) + rx_adj_fact = ins.channel_adjustment_factors_rx( + pol, is_second_band=True) + npt.assert_allclose(rx_adj_fact, adj_fact_rx[pol], + err_msg=( + 'Wrong RX channel adjustment factors ' + f'of second band for "{pol}"' + ))