From b333cbae75c42f9bbb77be4206de49a73df49308 Mon Sep 17 00:00:00 2001 From: Thomas Morris Date: Tue, 20 Feb 2024 21:33:58 -0500 Subject: [PATCH] flexible detector configs --- docs/source/arrays.rst | 21 --- docs/source/customizing.rst | 9 - docs/source/index.rst | 1 - docs/source/instruments.rst | 57 ++++++ docs/source/simulations.rst | 7 +- docs/source/usage.rst | 8 +- maria/atmosphere/__init__.py | 4 +- maria/atmosphere/turbulent_layer.py | 2 +- maria/instrument/__init__.py | 18 +- maria/instrument/bands.py | 28 +-- maria/instrument/bands.yml | 0 maria/instrument/bands/act.yml | 14 +- maria/instrument/bands/alma.yml | 20 +- maria/instrument/bands/atlast.yml | 12 +- maria/instrument/dets.py | 173 +++++++++++++----- maria/instrument/instruments.yml | 69 +++---- maria/noise/__init__.py | 2 +- maria/sim/default_params.yml | 2 +- maria/tests/test_instruments.py | 38 ++++ .../{test_objects.py => test_pointings.py} | 0 maria/tests/test_sites.py | 18 ++ maria/tod/__init__.py | 2 +- maria/utils/io.py | 10 +- 23 files changed, 350 insertions(+), 165 deletions(-) delete mode 100644 docs/source/arrays.rst delete mode 100644 docs/source/customizing.rst create mode 100644 docs/source/instruments.rst delete mode 100644 maria/instrument/bands.yml create mode 100644 maria/tests/test_instruments.py rename maria/tests/{test_objects.py => test_pointings.py} (100%) create mode 100644 maria/tests/test_sites.py diff --git a/docs/source/arrays.rst b/docs/source/arrays.rst deleted file mode 100644 index 307cf48..0000000 --- a/docs/source/arrays.rst +++ /dev/null @@ -1,21 +0,0 @@ -Customizing instruments -++++++++++++++++++ - -The ``instrument`` determines the characteristics of the observing instrument. We can make an instrument with:: - - my_instrument = maria.get_instrument('AtLAST') - - - -Simulations are made up of an instrument (defining the instrument), a site (where on earth it is), and a pointing (defining how it observes). Simulations for a few different telescopes might be instantiated as:: - - # MUSTANG-2 - mustang2_sim = Simulation(instrument='MUSTANG-2', - site='green_bank', - pointing='daisy', - map_file=path_to_some_fits_file, # Input files must be a fits file. - map_units='Jy/pixel', # Units of the input map in Kelvin Rayleigh Jeans (K, defeault) or Jy/pixel - map_res=pixel_size, - map_center=(10, 4.5), # RA & Dec. in degrees - map_freqs=[90], - degrees=True) diff --git a/docs/source/customizing.rst b/docs/source/customizing.rst deleted file mode 100644 index 337c4fc..0000000 --- a/docs/source/customizing.rst +++ /dev/null @@ -1,9 +0,0 @@ -Customizing -=========== - -.. toctree:: - :maxdepth: 2 - - instruments.rst - sites.rst - pointings.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 027d0ac..31aced7 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,7 +14,6 @@ a-flyin’ `_ :maxdepth: 2 installation.rst - sites.rst usage.rst tutorials.rst papers.rst diff --git a/docs/source/instruments.rst b/docs/source/instruments.rst new file mode 100644 index 0000000..3e74f9b --- /dev/null +++ b/docs/source/instruments.rst @@ -0,0 +1,57 @@ +=========== +Instruments +=========== + +The observing instrument is instantiated as an ``Instrument``. +The simplest way to get an instrument is to grab a pre-defined one, e.g.:: + + # The Atacama Cosmology Telescope + act = maria.get_instrument('ACT') + + # The Atacama Large Millimeter Array + alma = maria.get_instrument('ALMA') + + # MUSTANG-2 + m2 = maria.get_instrument('MUSTANG-2') + + ++++++++++++++++++++++++ +Customizing Instruments ++++++++++++++++++++++++ + +One way to customize instruments is to load a pre-defined instrument and pass extra parameters. +For example, we can give ACT twice the resolution with + +.. code-block:: python + + act = maria.get_instrument('ACT', primary_size=12) + + +Custom arrays of detectors are a bit more complicated. For example: + +.. code-block:: python + + f090 = {"center": 90, "width": 30} + f150 = {"center": 150, "width": 30} + + dets = {"n": 500, + "field_of_view": 2 + "array_shape": "hex", + "bands": [f090, f150]} + + my_custom_array = maria.get_instrument(dets=dets) + +Actually, there are several valid ways to define an array of detectors. +These are all valid: + +.. code-block:: python + + f090 = {"center": 90, "width": 30} + f150 = {"center": 150, "width": 30} + + dets = {"n": 500, + "field_of_view": 2 + "array_shape": "hex", + "bands": [f090, f150]} + + my_custom_array = maria.get_instrument(dets=dets) diff --git a/docs/source/simulations.rst b/docs/source/simulations.rst index 85fbe91..a5e13ef 100644 --- a/docs/source/simulations.rst +++ b/docs/source/simulations.rst @@ -1,5 +1,6 @@ +########### Simulations -+++++++++++ +########### We simulate observations by defining a ``Simulation`` object and running it to produce a ``TOD`` object:: @@ -16,7 +17,7 @@ The same simulation can produce any number of ``TOD`` objects, continuing from w yet_another_tod = sim.run() - ++++++++++++++++++++++++ Customizing simulations +++++++++++++++++++++++ @@ -47,7 +48,7 @@ We can also pass other arguments to the ``Simulation``, which will overwrite the } field_of_view=0.8 baseline=0 - geometry=0 + shape=0 primary_size=6 start_time=2022-02-10T06:00:00 integration_time=60 # in seconds diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 89555c7..b4c0e69 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -5,7 +5,7 @@ Usage :maxdepth: 2 simulations.rst - .. maps.rst - .. arrays.rst - .. sites.rst - .. pointings.rst + instruments.rst + pointings.rst + sites.rst + maps.rst diff --git a/maria/atmosphere/__init__.py b/maria/atmosphere/__init__.py index 6688f21..fb6880f 100644 --- a/maria/atmosphere/__init__.py +++ b/maria/atmosphere/__init__.py @@ -116,7 +116,7 @@ def _simulate_atmospheric_emission(self, units="K_RJ"): ) for band in bands: - band_index = self.instrument.dets(band=band.name).index + band_index = self.instrument.dets.subset(band=band.name).index if self.verbose: bands.set_description(f"Computing atm. emission ({band.name})") @@ -167,7 +167,7 @@ def _simulate_atmospheric_emission(self, units="K_RJ"): ) for band in bands: - band_index = self.instrument.dets(band=band.name).index + band_index = self.instrument.dets.subset(band=band.name).index if self.verbose: bands.set_description(f"Computing atm. transmission ({band.name})") diff --git a/maria/atmosphere/turbulent_layer.py b/maria/atmosphere/turbulent_layer.py index cbf0750..52bfbb0 100644 --- a/maria/atmosphere/turbulent_layer.py +++ b/maria/atmosphere/turbulent_layer.py @@ -309,7 +309,7 @@ def sample(self): for band in self.instrument.bands: # we assume the atmosphere looks the same for every nu in the band - band_index = self.instrument.dets(band=band.name).index + band_index = self.instrument.dets.subset(band=band.name).index band_angular_fwhm = self.instrument.angular_fwhm(z=self.depth)[ band_index ].mean() diff --git a/maria/instrument/__init__.py b/maria/instrument/__init__.py index 098ede7..c5a9f95 100644 --- a/maria/instrument/__init__.py +++ b/maria/instrument/__init__.py @@ -64,11 +64,10 @@ class Instrument: """ description: str = "" - primary_size: float = 5 # in meters - field_of_view: float = 1 # in deg - geometry: str = "hex" - baseline: float = 0 - bath_temp: float = 0 + primary_size: float = None # in meters + field_of_view: float = None # in deg + baseline: float = None + bath_temp: float = None bands: BandList = None dets: pd.DataFrame = None # dets, it's complicated documentation: str = "" @@ -85,6 +84,13 @@ def from_config(cls, config): return cls(bands=dets.bands, dets=dets, **config) + # def __post_init__(self): + + # if self.baseline is None: + # unique_baselines = sorted(np.unique(self.dets.baseline)) + + # ... + def __repr__(self): nodef_f_vals = ( (f.name, attrgetter(f.name)(self)) for f in fields(self) if f.name != "dets" @@ -93,7 +99,7 @@ def __repr__(self): nodef_f_repr = [] for name, value in nodef_f_vals: if name == "bands": - nodef_f_repr.append(f"{name}={value.__short_repr__()}") + nodef_f_repr.append(f"bands=[{', '.join(value.names)}]") else: nodef_f_repr.append(f"{name}={value}") diff --git a/maria/instrument/bands.py b/maria/instrument/bands.py index f031b2f..680fe15 100644 --- a/maria/instrument/bands.py +++ b/maria/instrument/bands.py @@ -12,7 +12,11 @@ BAND_FIELD_TYPES = { "center": "float", "width": "float", - "passband_shape": "str", + "shape": "str", + "tau": "float", + "white_noise": "float", + "pink_noise": "float", + "efficiency": "float", } here, this_filename = os.path.split(__file__) @@ -97,7 +101,7 @@ class Band: tau: float = 0.0 white_noise: float = 0.0 pink_noise: float = 0.0 - passband_shape: str = "gaussian" + shape: str = "gaussian" efficiency: float = 1.0 @classmethod @@ -107,7 +111,7 @@ def from_passband(cls, name, nu, pb, pb_err=None): nu[pb > pb.max() / np.e**2].ptp(), 3 ) # width is the two-sigma interval - band = cls(name=name, center=center, width=width, passband_shape="custom") + band = cls(name=name, center=center, width=width, shape="custom") band._nu = nu band._pb = pb @@ -116,20 +120,20 @@ def from_passband(cls, name, nu, pb, pb_err=None): @property def nu_min(self) -> float: - if self.passband_shape == "flat": + if self.shape == "flat": return self.center - 0.5 * self.width - if self.passband_shape == "gaussian": + if self.shape == "gaussian": return self.center - self.width - if self.passband_shape == "custom": + if self.shape == "custom": return self._nu[self._pb > 1e-2 * self._pb.max()].min() @property def nu_max(self) -> float: - if self.passband_shape == "flat": + if self.shape == "flat": return self.center + 0.5 * self.width - if self.passband_shape == "gaussian": + if self.shape == "gaussian": return self.center + self.width - if self.passband_shape == "custom": + if self.shape == "custom": return self._nu[self._pb > 1e-2 * self._pb.max()].max() def passband(self, nu): @@ -139,13 +143,13 @@ def passband(self, nu): _nu = np.atleast_1d(nu) - if self.passband_shape == "gaussian": + if self.shape == "gaussian": band_sigma = self.width / 4 return np.exp(-0.5 * np.square((_nu - self.center) / band_sigma)) - if self.passband_shape == "flat": + if self.shape == "flat": return np.where((_nu > self.nu_min) & (_nu < self.nu_max), 1.0, 0.0) - elif self.passband_shape == "custom": + elif self.shape == "custom": return np.interp(_nu, self._nu, self._pb) diff --git a/maria/instrument/bands.yml b/maria/instrument/bands.yml deleted file mode 100644 index e69de29..0000000 diff --git a/maria/instrument/bands/act.yml b/maria/instrument/bands/act.yml index 2b0a3fa..72cba8b 100644 --- a/maria/instrument/bands/act.yml +++ b/maria/instrument/bands/act.yml @@ -2,7 +2,7 @@ pa4: f150: center: 150 width: 30 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 266.e-6 pink_noise: 1.e-1 @@ -10,7 +10,7 @@ pa4: f220: center: 220 width: 30 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 266.e-6 pink_noise: 1.e-1 @@ -19,7 +19,7 @@ pa5: f090: center: 90 width: 30 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 266.e-6 pink_noise: 1.e-1 @@ -27,7 +27,7 @@ pa5: f150: center: 150 width: 30 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 266.e-6 pink_noise: 1.e-1 @@ -36,7 +36,7 @@ pa6: f090: center: 90 width: 30 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 266.e-6 pink_noise: 1.e-1 @@ -44,7 +44,7 @@ pa6: f150: center: 150 width: 30 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 266.e-6 pink_noise: 1.e-1 @@ -53,7 +53,7 @@ pa6: f093: n_dets: 217 field_of_view: 0.07 - detector_geometry: hex + array_shape: hex bands: [mustang2/f093] diff --git a/maria/instrument/bands/alma.yml b/maria/instrument/bands/alma.yml index c1b309e..831d8a3 100644 --- a/maria/instrument/bands/alma.yml +++ b/maria/instrument/bands/alma.yml @@ -1,7 +1,7 @@ f043: center: 43 width: 16 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 0 pink_noise: 0 @@ -10,7 +10,7 @@ f043: f078: center: 78 width: 22 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 0 pink_noise: 0 @@ -19,7 +19,7 @@ f078: f100: center: 100 width: 32 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 0 pink_noise: 0 @@ -28,7 +28,7 @@ f100: f144: center: 144 width: 38 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 0 pink_noise: 0 @@ -37,7 +37,7 @@ f144: f187: center: 187 width: 48 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 0 pink_noise: 0 @@ -46,7 +46,7 @@ f187: f243: center: 243 width: 64 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 0 pink_noise: 0 @@ -55,7 +55,7 @@ f243: f324: center: 324 width: 98 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 0 pink_noise: 0 @@ -64,7 +64,7 @@ f324: f447: center: 447 width: 114 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 0 pink_noise: 0 @@ -73,7 +73,7 @@ f447: f661: center: 661 width: 118 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 0 pink_noise: 0 @@ -82,7 +82,7 @@ f661: f869: center: 869 width: 163 - passband_shape: gaussian + shape: gaussian tau: 0 white_noise: 0 pink_noise: 0 diff --git a/maria/instrument/bands/atlast.yml b/maria/instrument/bands/atlast.yml index 53f292b..bc76f36 100644 --- a/maria/instrument/bands/atlast.yml +++ b/maria/instrument/bands/atlast.yml @@ -1,7 +1,7 @@ f027: center: 27 width: 5 - passband_shape: gaussian + shape: gaussian tau: 0 efficiency: 1.0 white_noise: 0 @@ -10,7 +10,7 @@ f027: f039: center: 39 width: 5 - passband_shape: gaussian + shape: gaussian tau: 0 efficiency: 1.0 white_noise: 0 @@ -19,7 +19,7 @@ f039: f093: center: 93 width: 10 - passband_shape: gaussian + shape: gaussian tau: 0 efficiency: 1.0 white_noise: 0 @@ -28,7 +28,7 @@ f093: f150: center: 150 width: 10 - passband_shape: gaussian + shape: gaussian tau: 0 efficiency: 1.0 white_noise: 0 @@ -37,7 +37,7 @@ f150: f225: center: 225 width: 30 - passband_shape: gaussian + shape: gaussian tau: 0 efficiency: 1.0 white_noise: 0 @@ -46,7 +46,7 @@ f225: f280: center: 280 width: 40 - passband_shape: gaussian + shape: gaussian tau: 0 efficiency: 1.0 white_noise: 0 diff --git a/maria/instrument/dets.py b/maria/instrument/dets.py index b358486..93aa868 100644 --- a/maria/instrument/dets.py +++ b/maria/instrument/dets.py @@ -30,16 +30,42 @@ "pol_angle": "float", "efficiency": "float", "primary_size": "float", + "bath_temp": "float", } -SUPPORTED_GEOMETRIES = ["flower", "hex", "square"] +SUPPORTED_GEOMETRIES = ["flower", "hex", "square", "circle"] -def generate_offsets(n, geometry="hex"): - if geometry not in SUPPORTED_GEOMETRIES: - raise ValueError(f"'geometry' must be one of {SUPPORTED_GEOMETRIES}.") +def hex_packed_circle_offsets(n): + """ + Returns an array of $n$ hexagonal offsets from the origin with diameter 1. + """ - if geometry == "hex": + h = int(np.ceil((np.sqrt(12 * n - 3) + 3) / 6)) + + side = np.arange(-h, h + 1, dtype=float) + x, y = np.meshgrid(side, side) + + x[1::2] -= 0.5 + y *= np.sqrt(3) / 2 + + offsets = np.c_[x.ravel(), y.ravel()] + + distance_squared = np.sum(offsets**2, axis=1) + + subset_index = np.argsort(distance_squared)[:n] + + return 0.5 * offsets[subset_index] / np.sqrt(distance_squared[subset_index[-1]]) + + +def generate_offsets(n, shape="hex"): + if shape not in SUPPORTED_GEOMETRIES: + raise ValueError(f"'shape' must be one of {SUPPORTED_GEOMETRIES}.") + + if shape == "circle": + return hex_packed_circle_offsets(n).T + + if shape == "hex": angles = np.linspace(0, 2 * np.pi, 6 + 1)[1:] + np.pi / 2 z = np.array([0]) layer = 0 @@ -55,7 +81,7 @@ def generate_offsets(n, geometry="hex"): return np.c_[np.real(np.array(z[:n])), np.imag(np.array(z[:n]))].T - if geometry == "flower": + if shape == "flower": golden_ratio = np.pi * (3.0 - np.sqrt(5.0)) # golden angle in radians z = np.zeros(n).astype(complex) for i in range(n): @@ -63,27 +89,28 @@ def generate_offsets(n, geometry="hex"): z *= 0.5 / np.abs(z.max()) return np.c_[np.real(z), np.imag(z)].T - if geometry == "square": + if shape == "square": side = np.linspace(-0.5, 0.5, int(np.ceil(np.sqrt(n)))) DX, DY = np.meshgrid(side, side) return np.c_[DX.ravel()[:n], DY.ravel()[:n]].T def generate_dets( - n: int = 1, + n: int, + bands: list, field_of_view: float = 0.0, - boresight_offset: tuple = (0.0, 0.0), - detector_geometry: tuple = "hex", + array_offset: tuple = (0.0, 0.0), + array_shape: tuple = "hex", baseline_offset: tuple = (0.0, 0.0, 0.0), baseline_diameter: float = 0.0, - baseline_geometry: str = "flower", - bands: list = [], + baseline_shape: str = "flower", + **kwargs, ): dets = pd.DataFrame() - detector_offsets = field_of_view * generate_offsets(n=n, geometry=detector_geometry) + detector_offsets = field_of_view * generate_offsets(n=n, shape=array_shape) - baselines = baseline_diameter * generate_offsets(n=n, geometry=baseline_geometry) + baselines = baseline_diameter * generate_offsets(n=n, shape=baseline_shape) for band in bands: band_dets = pd.DataFrame( @@ -91,8 +118,8 @@ def generate_dets( ) band_dets.loc[:, "band"] = band - band_dets.loc[:, "offset_x"] = boresight_offset[0] + detector_offsets[0] - band_dets.loc[:, "offset_y"] = boresight_offset[1] + detector_offsets[1] + band_dets.loc[:, "offset_x"] = array_offset[0] + detector_offsets[0] + band_dets.loc[:, "offset_y"] = array_offset[1] + detector_offsets[1] band_dets.loc[:, "baseline_x"] = baseline_offset[0] + baselines[0] band_dets.loc[:, "baseline_y"] = baseline_offset[1] + baselines[1] @@ -103,32 +130,85 @@ def generate_dets( return dets +def validate_band_config(band): + if any([key not in band for key in ["center"]]): + raise ValueError("The band center must be specified") + + +def parse_bands_config(bands): + """ + There are many ways to specify bands, and this handles them. + """ + parsed_band_config = {} + + if isinstance(bands, list): + for band in bands: + if isinstance(band, str): + if band not in all_bands: + raise ValueError(f'Band "{band}" is not supported.') + parsed_band_config[band] = all_bands[band] + + if isinstance(band, Mapping): + validate_band_config(band) + name = band.get("name", f'f{int(band["center"]):>03}') + parsed_band_config[name] = band + + if isinstance(bands, Mapping): + for name, band in bands.items(): + validate_band_config(band) + parsed_band_config[name] = band + + return parsed_band_config + + class Detectors: @classmethod def from_config(cls, config): - dets_config = utils.io.flatten_config(config["dets"]) + """ + Instantiate detectors from a config. We pass the whole config and not just config["dets"] so + that the detectors can inherit instrument parameters if need be. + """ + + config["dets"] = utils.io.flatten_config(config["dets"]) + + bands_config = {} df = pd.DataFrame( {col: pd.Series(dtype=dtype) for col, dtype in DET_COLUMN_TYPES.items()} ) - for tag, dets_config in utils.io.flatten_config(config["dets"]).items(): - if "file" in dets_config: - tag_df = pd.read_csv(f'{here}/{dets_config["file"]}', index_col=0) + for tag in config["dets"]: + tag_dets_config = config["dets"][tag] + + if "file" in tag_dets_config: + # if a file is supplied, assume it's a csv and read it in + tag_df = pd.read_csv(f'{here}/{tag_dets_config["file"]}', index_col=0) + + # if no bands were supplied, get them from the table and hope they're registered + if "bands" not in tag_dets_config: + tag_dets_config["bands"] = sorted(np.unique(tag_df.band.values)) + + tag_dets_config["bands"] = parse_bands_config(tag_dets_config["bands"]) else: - tag_df = generate_dets(**dets_config) + tag_dets_config["bands"] = parse_bands_config(tag_dets_config["bands"]) + tag_df = generate_dets(**tag_dets_config) fill_level = int(np.log(len(tag_df) - 1) / np.log(10) + 1) - tag_df.insert( - 0, - "uid", - [f"{tag}_{str(i).zfill(fill_level)}" for i in range(len(tag_df))], - ) + + uid_predix = f"{tag}-" if tag else "" + uids = [ + f"{uid_predix}{str(i).zfill(fill_level)}" for i in range(len(tag_df)) + ] + + tag_df.insert(0, "uid", uids) tag_df.insert(1, "tag", tag) df = pd.concat([df, tag_df]) + for band, band_config in tag_dets_config["bands"].items(): + bands_config[band] = band_config + df.index = np.arange(len(df)) for col in ["primary_size"]: @@ -142,18 +222,11 @@ def from_config(cls, config): "baseline_y", "baseline_z", "pol_angle", + "bath_temp", ]: if df.loc[:, col].isna().any(): df.loc[:, col] = 0 - # get the bands - bands_config = {} - for band in sorted(np.unique(df.band)): - if isinstance(band, Mapping): - bands_config[band] = band - if isinstance(band, str): - bands_config[band] = all_bands[band] - bands = BandList.from_config(bands_config) for band in bands: @@ -176,9 +249,9 @@ def __getattr__(self, attr): if attr in self.df.columns: return self.df.loc[:, attr].values.astype(DET_COLUMN_TYPES[attr]) - def __call__(self, band=None): - if band is not None: - return self.subset(band=band) + # def __call__(self, band=None): + # if band is not None: + # return self.subset(band=band) def subset(self, band=None): bands = BandList(self.bands[band]) @@ -190,20 +263,24 @@ def n(self): return len(self.df) @property - def band_center(self): - centers = np.zeros(shape=self.n) - for band in self.bands: - centers[self.band == band.name] = band.center + def offset(self): + return np.c_[self.offset_x, self.offset_y] - return centers + # @property + # def band_center(self): + # centers = np.zeros(shape=self.n) + # for band in self.bands: + # centers[self.band == band.name] = band.center - @property - def band_width(self): - widths = np.zeros(shape=self.n) - for band in self.bands: - widths[self.band == band.name] = band.width + # return centers + + # @property + # def band_width(self): + # widths = np.zeros(shape=self.n) + # for band in self.bands: + # widths[self.band == band.name] = band.width - return widths + # return widths @property def __len__(self): diff --git a/maria/instrument/instruments.yml b/maria/instrument/instruments.yml index f0a7f29..0e54172 100644 --- a/maria/instrument/instruments.yml +++ b/maria/instrument/instruments.yml @@ -1,14 +1,26 @@ +default: + description: A simple test array + documentation: + dets: + n: 250 + field_of_view: 1 + array_shape: circle + bands: + f150: + center: 150 + width: 30 + primary_size: 5 + + ABS: aliases: ["abs"] description: Atacama B-Mode Search documentation: https://almascience.nrao.edu/about-alma/alma-basics dets: - abs: - field_of_view: 25 - detector_geometry: hex - n: 250 - bands: [abs/f150] - geometry: hex + n: 250 + field_of_view: 25 + array_shape: hex + bands: [abs/f150] primary_size: 0.5 @@ -19,21 +31,21 @@ AdvACT: dets: pa4: n: 750 - boresight_offset: [-0.8, -0.5] - field_of_view: 0.8 - detector_geometry: hex + array_offset: [-0.8, -0.5] + field_of_view: 1.0 + array_shape: hex bands: [act/pa4/f150, act/pa4/f220] pa5: n: 750 - boresight_offset: [0.0, 1.0] - field_of_view: 0.8 - detector_geometry: hex + array_offset: [0.0, 1.0] + field_of_view: 1.0 + array_shape: hex bands: [act/pa5/f090, act/pa5/f150] pa6: n: 750 - boresight_offset: [0.8, -0.5] - field_of_view: 0.8 - detector_geometry: hex + array_offset: [0.8, -0.5] + field_of_view: 1.0 + array_shape: hex bands: [act/pa6/f090, act/pa6/f150] primary_size: 6 @@ -42,35 +54,30 @@ ALMA: description: ALMA Configuration 1 documentation: https://www.eso.org/public/teles-instr/alma/ dets: - alma: - file: data/alma/alma.cycle1.total.csv + file: data/alma/alma.cycle1.total.csv primary_size: 12 AtLAST: description: Atacama Large Aperture Submillimeter Telescope dets: - atlast: - n: 217 - field_of_view: 0.07 - detector_geometry: hex - bands: [atlast/f027, atlast/f039, atlast/f093, atlast/f150, atlast/f225, atlast/f280] + n: 217 + field_of_view: 0.07 + array_shape: hex + bath_temp: 100.e-3 # in K + bands: [atlast/f027, atlast/f039, atlast/f093, atlast/f150, atlast/f225, atlast/f280] field_of_view: 0.07 # in degrees - geometry: hex primary_size: 100 # in meters - bath_temp: 100.e-3 # in K documentation: https://greenbankobservatory.org/science/gbt-observers/mustang-2/ MUSTANG-2: description: MUSTANG-2 (Multiplexed SQUID TES Array for Ninety Gigahertz) dets: - m2: - n: 217 - field_of_view: 0.07 - detector_geometry: hex - bands: [mustang2/f093] + n: 217 + field_of_view: 0.07 + array_shape: hex + bands: [mustang2/f093] + bath_temp: 100.e-3 # in K field_of_view: 0.07 # in degrees - geometry: hex primary_size: 100 # in meters - bath_temp: 100.e-3 # in K documentation: https://www.atlast.uio.no/ diff --git a/maria/noise/__init__.py b/maria/noise/__init__.py index 5aa75b6..6e7d720 100644 --- a/maria/noise/__init__.py +++ b/maria/noise/__init__.py @@ -11,7 +11,7 @@ def _simulate_noise(self): self.data["noise"] = np.zeros((self.instrument.n_dets, self.pointing.n_time)) for band in self.instrument.dets.bands: - band_index = self.instrument.dets(band=band.name).index + band_index = self.instrument.dets.subset(band=band.name).index if band.white_noise > 0: self.data["noise"][band_index] += ( diff --git a/maria/sim/default_params.yml b/maria/sim/default_params.yml index 7925e1c..30ec631 100644 --- a/maria/sim/default_params.yml +++ b/maria/sim/default_params.yml @@ -12,7 +12,7 @@ instrument: # defaults to a small test instrument bands: ["act/pa5/f150"] field_of_view: 0.8 baseline: 0 # meters - geometry: hex + shape: hex primary_size: 6 az_bounds: [0, 360] el_bounds: [20, 90] diff --git a/maria/tests/test_instruments.py b/maria/tests/test_instruments.py new file mode 100644 index 0000000..a81c54c --- /dev/null +++ b/maria/tests/test_instruments.py @@ -0,0 +1,38 @@ +import pytest + +import maria + +f090 = {"center": 90, "width": 10} +f150 = {"center": 150, "width": 20} + +dets1 = { + "array1": { + "n": 500, + "field_of_view": 2, + "array_shape": "hex", + "bands": [f090, f150], + }, + "array2": { + "n": 500, + "field_of_view": 2, + "array_shape": "hex", + "bands": [f090, f150], + }, +} + +dets2 = { + "n": 500, + "field_of_view": 2, + "array_shape": "hex", + "bands": {"f090": {"center": 90, "width": 10}}, +} + + +@pytest.mark.parametrize("instrument_name", maria.all_instruments) +def test_get_instrument(instrument_name): + instrument = maria.get_instrument(instrument_name) + + +@pytest.mark.parametrize("dets", [dets1, dets2]) +def test_get_custom_array(dets): + instrument = maria.get_instrument(dets=dets) diff --git a/maria/tests/test_objects.py b/maria/tests/test_pointings.py similarity index 100% rename from maria/tests/test_objects.py rename to maria/tests/test_pointings.py diff --git a/maria/tests/test_sites.py b/maria/tests/test_sites.py new file mode 100644 index 0000000..d41520c --- /dev/null +++ b/maria/tests/test_sites.py @@ -0,0 +1,18 @@ +import pytest + +import maria + + +@pytest.mark.parametrize("instrument_name", maria.all_instruments) +def test_get_instrument(instrument_name): + instrument = maria.get_instrument(instrument_name) + + +@pytest.mark.parametrize("pointing_name", maria.all_pointings) +def test_get_pointing(pointing_name): + pointing = maria.get_pointing(pointing_name) + + +@pytest.mark.parametrize("site_name", maria.all_sites) +def test_get_site(site_name): + site = maria.get_site(site_name) diff --git a/maria/tod/__init__.py b/maria/tod/__init__.py index 292d753..c38825c 100644 --- a/maria/tod/__init__.py +++ b/maria/tod/__init__.py @@ -79,7 +79,7 @@ def _from_mustang2(cls, fname: str, hdu: int = 1): ) m2_config = instrument.get_instrument_config(instrument_name="MUSTANG-2") - m2_config["dets"]["m2"]["n"] = n_dets + m2_config["dets"]["n"] = n_dets dets = Detectors.from_config(m2_config) diff --git a/maria/utils/io.py b/maria/utils/io.py index 7b8746f..19b6fe9 100644 --- a/maria/utils/io.py +++ b/maria/utils/io.py @@ -10,10 +10,18 @@ def flatten_config(m, prefix=""): + """ + Turn any dict into a mapping of mappings. + """ + # if too shallow, add a dummy index + if not all(isinstance(v, Mapping) for k, v in m.items()): + return flatten_config({"": m}, prefix="") + + # recursion! items = [] for k, v in m.items(): new_key = f"{prefix}/{k}" if prefix else k - if any(isinstance(vv, Mapping) for kk, vv in v.items()): + if all(isinstance(vv, Mapping) for kk, vv in v.items()): items.extend(flatten_config(v, new_key).items()) else: items.append((new_key, v))