From 092b23e216a8f77b4890bab5cd021801dcefa8e1 Mon Sep 17 00:00:00 2001 From: Alexandra Rahlin Date: Tue, 2 Apr 2024 15:40:49 -0500 Subject: [PATCH] Interface for consistent bolometer band string formatting (#151) This enables, e.g., handling spectrometer channels with closely spaced frequencies for mapmaking. --- calibration/CMakeLists.txt | 1 + calibration/README.rst | 4 + calibration/python/bolopropertiesutils.py | 175 ++++++++++++++++++++-- calibration/tests/band_format.py | 30 ++++ 4 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 calibration/tests/band_format.py diff --git a/calibration/CMakeLists.txt b/calibration/CMakeLists.txt index 0873899f..31e5289f 100644 --- a/calibration/CMakeLists.txt +++ b/calibration/CMakeLists.txt @@ -5,3 +5,4 @@ target_link_libraries(calibration core) link_python_dir() add_spt3g_test(can_modify_bolo_props_in_map SLOWTEST) +add_spt3g_test(band_format) diff --git a/calibration/README.rst b/calibration/README.rst index a785d388..9a09b3c3 100644 --- a/calibration/README.rst +++ b/calibration/README.rst @@ -7,3 +7,7 @@ The calibration project contains data classes and analysis code for storing the *Physical properties of the bolometers* This includes the relative pointing offsets of the detectors on the focal plane, their polarization angles and efficiences, their bands, and the fabrication name (physical name) of the detector to which the channel corresponds. It does *not* include tuning-dependent parameters like time constants or responsivity. This information is stored in a ``BolometerPropertiesMap``, indexed by logical bolometer ID, in the Calibration frame. The standard name for this object is "BolometerProperties" and it is created from a composite of other calibration information. + The frequency band for a bolometer is often represented by a string name with units. The ``BandFormat`` class provides some functions for converting numerical bands (in G3Units) to their string representation. A global instance of this class is instantiated, for ensuring consistent band formatting throughout the library code. By default, the band string is formatted with precision 0 (i.e. an integer) in units of GHz; these can be modified by using the ``set_band_format()`` function. The ``band_to_string()`` and ``band_to_value()`` functions can be used to convert numerical band values to their string representation and vice versa. The ``extract_band_string()`` and ``extract_band_value()`` functions can be used to extract band information in string or numerical form from other strings. Library code should take care to use these functions wherever possible. + + In addition, the ``.band_string`` property of a ``BolometerProperties`` instance returns the string representation of the given bolometer's frequency band, using the global formatting specification. + diff --git a/calibration/python/bolopropertiesutils.py b/calibration/python/bolopropertiesutils.py index bfac8546..72c9dbd0 100644 --- a/calibration/python/bolopropertiesutils.py +++ b/calibration/python/bolopropertiesutils.py @@ -1,9 +1,166 @@ from spt3g.calibration import BolometerProperties from spt3g import core -import math +import numpy as np +import re + +__all__ = [ + "SplitByProperty", + "SplitByBand", + "SplitTimestreamsByBand", + "SplitByWafer", + "SplitByPixelType", + "BandFormat", + "get_band_units", + "set_band_format", + "band_to_string", + "band_to_value", + "extract_band_string", + "extract_band_value", +] + + +# framework for handling frequency band formatting + +class BandFormat: + """ + Class for converting a frequency band between a quantity in G3Units and its + string representation. + + Arguments + --------- + precision : int + Float formatting precision for the band quantity. If <=0, output will + be an integer. If >0, output will be a floating point number with this + many decimal places of precision. + units : str + String name of the G3Units quantity in which to represent the frequency + band, e.g. "GHz" or "MHz". Must correspond to a valid attribute of the + core.G3Units namespace. + """ + + def __init__(self, precision=0, units="GHz"): + self.set_format(precision, units) + + def set_format(self, precision=0, units="GHz"): + """ + Set the band format precision and units for converting between a + quantity in G3Units and its string representation. + + Arguments + --------- + precision : int + Float formatting precision for the band quantity. If <=0, output + will be an integer. If >0, output will be a floating point number + with this many decimal places of precision. + units : str + String name of the G3Units quantity in which to represent the + frequency band, e.g. "GHz" or "MHz". Must correspond to a valid + attribute of the core.G3Units namespace. + """ + self._precision = int(precision) + assert hasattr(core.G3Units, units), "Invalid units {}".format(units) + self._uvalue = getattr(core.G3Units, units) + self._vformat = "%%.%df" % (precision if precision > 0 else 0) + self._uformat = units + self._format = "%s%s" % (self._vformat, units) + prx = r"\.[0-9]{%d}" % precision if precision > 0 else "" + self._pattern = "([0-9]+%s)%s" % (prx, units) + self._wregex = re.compile("^" + self._pattern + "$") + self._regex = re.compile(self._pattern) + + @property + def units(self): + """Units string used for formatting""" + return self._uformat + + def to_string(self, value, include_units=True): + """Convert a band value in G3Units to its string representation, using + the appropriate precision and units name.""" + if not np.isfinite(value) or value < 0: + return "" + value = np.round(value / self._uvalue, self._precision) + if not include_units: + return self._vformat % value + return self._format % value + + def to_value(self, string): + """Convert a band string to a value in G3Units, or raise a ValueError + if no match is found.""" + m = self._wregex.match(string) + if not m: + raise ValueError("Invalid band {}".format(string)) + return float(m.group(1)) * self._uvalue + + def extract_string(self, string, include_units=True): + """Return the band substring from the input string, or None if not + found.""" + m = self._regex.match(string) + if not m: + return None + if not include_units: + return m.group(1) + return m.group(0) + + def extract_value(self, value): + """Return the band in G3Units extracted from the input string, or None + if not found.""" + s = extract_string(value, include_units=False) + if not s: + return None + return float(v) * self._uvalue + + +# global instance used by functions below +_band_format = BandFormat() + + +@core.usefulfunc +def get_band_units(): + """Return the units string used for formatting frequency band values.""" + return _band_format.units + + +@core.usefulfunc +def set_band_format(precision, units): + _band_format.set_format(precision, units) +set_band_format.__doc__ = BandFormat.set_format.__doc__ + + +@core.usefulfunc +def band_to_string(value, include_units=True): + return _band_format.to_string(value, include_units=include_units) +band_to_string.__doc__ = BandFormat.to_string.__doc__ + + +@core.usefulfunc +def band_to_value(string): + return _band_format.to_value(string) +band_to_value.__doc__ = BandFormat.to_value.__doc__ + + +@core.usefulfunc +def extract_band_string(string, include_units=True): + return _band_format.extract_string(string, include_units=include_units) +extract_band_string.__doc__ = BandFormat.extract_string.__doc__ + + +@core.usefulfunc +def extract_band_value(string): + return _band_format.extract_value(string) +extract_band_value.__doc__ = BandFormat.extract_value.__doc__ + + +# monkeypatch useful property attributes +def band_string(self): + """String representation of frequency band center""" + return _band_format.to_string(self.band) +BolometerProperties.band_string = property(band_string) + +def band_vstring(self): + """String representation of frequency band center, without units name""" + return _band_format.to_string(self.band, include_units=False) +BolometerProperties.band_vstring = property(band_vstring) -__all__ = ['SplitByProperty', 'SplitByBand', 'SplitTimestreamsByBand', - 'SplitByWafer', 'SplitByPixelType'] @core.indexmod class SplitByProperty(object): @@ -125,17 +282,15 @@ def __init__(self, input='CalTimestreams', output_root=None, ''' super(SplitByBand, self).__init__( input=input, output_root=output_root, property_list=bands, - bpm=bpm, property='band', drop_empty=drop_empty) + bpm=bpm, property='band_string', drop_empty=drop_empty) @staticmethod - def converter(band): - if isinstance(band, str): - return band - if math.isnan(band) or math.isinf(band): + def converter(band_string): + if band_string is None: return None - if band < 0: + if not band_string: return None - return '%dGHz' % int(band/core.G3Units.GHz) + return str(band_string) @core.indexmod diff --git a/calibration/tests/band_format.py b/calibration/tests/band_format.py new file mode 100644 index 00000000..4db67672 --- /dev/null +++ b/calibration/tests/band_format.py @@ -0,0 +1,30 @@ +from spt3g import core +from spt3g.calibration import BolometerProperties, set_band_format, band_to_value, get_band_units + +bp = BolometerProperties() +bp.band = 94.67 * core.G3Units.GHz + +assert bp.band_string == "95GHz" +assert bp.band_vstring == "95" +assert band_to_value(bp.band_string) == 95 * core.G3Units.GHz +assert bp.band_vstring + get_band_units() == bp.band_string + +set_band_format(0, "MHz") + +assert bp.band_string == "94670MHz" +assert bp.band_vstring == "94670" +assert band_to_value(bp.band_string) == bp.band +assert bp.band_vstring + get_band_units() == bp.band_string + +set_band_format(2, "GHz") + +assert bp.band_string == "94.67GHz" +assert bp.band_vstring == "94.67" +assert band_to_value(bp.band_string) == bp.band +assert bp.band_vstring + get_band_units() == bp.band_string + +set_band_format(-1, "GHz") +assert bp.band_string == "90GHz" +assert bp.band_vstring == "90" +assert band_to_value(bp.band_string) == 90 * core.G3Units.GHz +assert bp.band_vstring + get_band_units() == bp.band_string