Source code for sotodlib.sim_hardware

# Copyright (c) 2018-2019 Simons Observatory.
# Full license can be found in the top level "LICENSE" file.
"""Focalplane simulation tools.
"""

import re

from collections import OrderedDict

from copy import deepcopy

import numpy as np

from .core import Hardware


[docs] def sim_detectors_toast(hw, tele, tube_slots=None): """Update hardware model with simulated detector positions. Given a Hardware model, generate all detector properties for the specified telescope and optionally a subset of optics tube slots (for the LAT). The detector dictionary of the hardware model is updated in place. This function requires the toast subpackage (and hence toast) to be importable. Args: hw (Hardware): The hardware object to update. tele (str): The telescope name. tube_slots (list, optional): The optional list of tube slots to include. Returns: None """ try: from . import toast as sotoast except ImportError: msg = "Toast package is not importable, cannot simulate detector positions" raise RuntimeError(msg) sotoast.sim_focalplane.sim_telescope_detectors( hw, tele, tube_slots=tube_slots, )
[docs] def sim_detectors_physical_optics(hw, tele, tube_slots=None): """Update hardware model with simulated detector positions. Given a Hardware model, generate all detector properties for the specified telescope and optionally a subset of optics tube slots (for the LAT). The detector dictionary of the hardware model is updated in place. This function uses information from physical optics simulations to estimate the location of detectors. Args: hw (Hardware): The hardware object to update. tele (str): The telescope name. tube_slots (list, optional): The optional list of tube slots to include. Returns: None """ raise NotImplementedError("Not yet implemented")
[docs] def sim_nominal(): """Return a simulated nominal hardware configuration. This returns a simulated Hardware object with the nominal instrument properties / metadata, but with an empty set of detector locations. This can then be passed to one of the detector simulation functions to build up the list of detectors. Returns: (Hardware): Hardware object with nominal metadata. """ cnf = OrderedDict() bands = OrderedDict() bnd = OrderedDict() bnd["center"] = 25.7 bnd["low"] = 21.7 bnd["high"] = 29.7 bnd["bandpass"] = "" bnd["NET"] = 435.1 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 # Noise elevation scaling fits from Carlos Sierra # These numbers are for V3 LAT baseline bnd["A"] = 0.06 bnd["C"] = 0.92 bnd["NET_corr"] = 1.10 bands["LAT_f030"] = bnd bnd = OrderedDict() bnd["center"] = 38.9 bnd["low"] = 30.9 bnd["high"] = 46.9 bnd["bandpass"] = "" bnd["NET"] = 281.5 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 bnd["A"] = 0.16 bnd["C"] = 0.79 bnd["NET_corr"] = 1.02 bands["LAT_f040"] = bnd bnd = OrderedDict() bnd["center"] = 92.0 bnd["low"] = 79.0 bnd["high"] = 105.0 bnd["bandpass"] = "" bnd["NET"] = 361.0 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 bnd["A"] = 0.16 bnd["C"] = 0.80 bnd["NET_corr"] = 1.09 bands["LAT_f090"] = bnd bnd = OrderedDict() bnd["center"] = 147.5 bnd["low"] = 130.0 bnd["high"] = 165.0 bnd["bandpass"] = "" bnd["NET"] = 352.4 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 bnd["A"] = 0.17 bnd["C"] = 0.78 bnd["NET_corr"] = 1.01 bands["LAT_f150"] = bnd bnd = OrderedDict() bnd["center"] = 225.7 bnd["low"] = 196.7 bnd["high"] = 254.7 bnd["bandpass"] = "" bnd["NET"] = 724.4 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 bnd["A"] = 0.29 bnd["C"] = 0.62 bnd["NET_corr"] = 1.02 bands["LAT_f230"] = bnd bnd = OrderedDict() bnd["center"] = 285.4 bnd["low"] = 258.4 bnd["high"] = 312.4 bnd["bandpass"] = "" bnd["NET"] = 1803.9 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 bnd["A"] = 0.36 bnd["C"] = 0.53 bnd["NET_corr"] = 1.00 bands["LAT_f290"] = bnd bnd = OrderedDict() bnd["center"] = 25.7 bnd["low"] = 21.7 bnd["high"] = 29.7 bnd["bandpass"] = "" bnd["NET"] = 314.1 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 # Noise elevation scaling fits from Carlos Sierra # These numbers are for V3 SAT baseline bnd["A"] = 0.06 bnd["C"] = 0.92 bnd["NET_corr"] = 1.06 bands["SAT_f030"] = bnd bnd = OrderedDict() bnd["center"] = 38.9 bnd["low"] = 30.9 bnd["high"] = 46.9 bnd["bandpass"] = "" bnd["NET"] = 225.8 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 bnd["A"] = 0.19 bnd["C"] = 0.76 bnd["NET_corr"] = 1.01 bands["SAT_f040"] = bnd bnd = OrderedDict() bnd["center"] = 92.0 bnd["low"] = 79.0 bnd["high"] = 105.0 bnd["bandpass"] = "" bnd["NET"] = 245.1 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 bnd["A"] = 0.19 bnd["C"] = 0.76 bnd["NET_corr"] = 1.04 bands["SAT_f090"] = bnd bnd = OrderedDict() bnd["center"] = 147.5 bnd["low"] = 130.0 bnd["high"] = 165.0 bnd["bandpass"] = "" bnd["NET"] = 250.2 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 bnd["A"] = 0.23 bnd["C"] = 0.70 bnd["NET_corr"] = 1.02 bands["SAT_f150"] = bnd bnd = OrderedDict() bnd["center"] = 225.7 bnd["low"] = 196.7 bnd["high"] = 254.7 bnd["bandpass"] = "" bnd["NET"] = 540.3 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 bnd["A"] = 0.35 bnd["C"] = 0.54 bnd["NET_corr"] = 1.00 bands["SAT_f230"] = bnd bnd = OrderedDict() bnd["center"] = 285.4 bnd["low"] = 258.4 bnd["high"] = 312.4 bnd["bandpass"] = "" bnd["NET"] = 1397.5 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 bnd["A"] = 0.42 bnd["C"] = 0.45 bnd["NET_corr"] = 1.00 bands["SAT_f290"] = bnd # Special "band" for dark bolometers bnd = OrderedDict() bnd["center"] = np.nan bnd["low"] = np.nan bnd["high"] = np.nan bnd["bandpass"] = "" bnd["NET"] = 1000.0 bnd["fknee"] = 50.0 bnd["fmin"] = 0.01 bnd["alpha"] = 1.0 bnd["A"] = np.nan bnd["C"] = np.nan bnd["NET_corr"] = 1.00 bands["NC"] = bnd cnf["bands"] = bands wafer_slots = OrderedDict() wtypes = ["LAT_UHF", "SAT_UHF", "LAT_MF", "SAT_MF", "LAT_LF", "SAT_LF"] wcnt = { "LAT_LF": 1*3, "LAT_MF": 4*3, "LAT_UHF": 2*3, "SAT_LF": 1*7, "SAT_MF": 2*7, "SAT_UHF": 1*7 } wnp = { "LAT_LF": 37, "LAT_MF": 432, "LAT_UHF": 432, "SAT_LF": 37, "SAT_MF": 432, "SAT_UHF": 432 } wpixmm = { "LAT_LF": 18.0, "LAT_MF": 5.3, "LAT_UHF": 5.3, "SAT_LF": 18.0, "SAT_MF": 5.3, "SAT_UHF": 5.3 } wrhombgap = { "LAT_MF": 0.71, "LAT_UHF": 0.71, "SAT_MF": 0.71, "SAT_UHF": 0.71 } wbd = { "LAT_LF": ["LAT_f030", "LAT_f040"], "LAT_MF": ["LAT_f090", "LAT_f150"], "LAT_UHF": ["LAT_f230", "LAT_f290"], "SAT_LF": ["SAT_f030", "SAT_f040"], "SAT_MF": ["SAT_f090", "SAT_f150"], "SAT_UHF": ["SAT_f230", "SAT_f290"] } windx = 0 cardindx = 0 for wt in wtypes: for ct in range(wcnt[wt]): wn = "w{:02d}".format(windx) wf = OrderedDict() wf["type"] = wt if ((wt == "LAT_LF") or (wt == "SAT_LF")): wf["packing"] = "S" else: wf["packing"] = "F" wf["rhombusgap"] = wrhombgap[wt] wf["npixel"] = wnp[wt] wf["pixsize"] = wpixmm[wt] wf["bands"] = wbd[wt] wf["card_slot"] = "card_slot{:02d}".format(cardindx) wf["wafer_name"] = "" cardindx += 1 wafer_slots[wn] = wf windx += 1 cnf["wafer_slots"] = wafer_slots tube_slots = OrderedDict() woff = { "LAT_LF": 0, "LAT_MF": 0, "LAT_UHF": 0, "SAT_LF": 0, "SAT_MF": 0, "SAT_UHF": 0 } ltubes = ["LAT_UHF", "LAT_UHF", "LAT_MF", "LAT_MF", "LAT_MF", "LAT_MF", "LAT_LF"] # The optics tubes are arranged using several conventions and here we map between # them. Note that for this hardware model, the projection on the sky is defined # at 60 degree elevation with no boresight rotation. # TOAST hexagon layout positions in Xi/Eta coordinates ltube_toasthex_pos = [0, 1, 2, 3, 5, 6, 10] # "Optics" locations as given in several SO slide decks. Note this is flipped in # some figures. See: # https://simonsobs.atlassian.net/wiki/spaces/PRO/pages/101974017/Focal+Plane+Coordinates ltube_optics_pos = [1, 3, 5, 4, 8, 9, 12] # Cryo team names for these positions ltube_cryonames=["c1", "i5", "i6", "i1", "i3", "i4", "o6"] lat_ufm_slot = [ 0, 1, 2, ] lat_ufm_thetarot = [ 240.0, 0.0, 120.0, ] for tindx in range(7): nm = ltube_cryonames[tindx] ttyp = ltubes[tindx] tb = OrderedDict() tb["type"] = ttyp tb["waferspace"] = 128.4 tb["wafer_slots"] = list() tb["wafer_slot_angle"] = [ lat_ufm_thetarot[tw] for tw in range(3) ] # Degrees # The "slot" here is the relative slot (ws0 - ws2) within the tube. tb["wafer_ufm_slot"] = list() for tw in range(3): off = 0 for w, props in cnf["wafer_slots"].items(): if props["type"] == ttyp: if off == woff[ttyp]: props["tube_index"] = tw tb["wafer_slots"].append(w) tb["wafer_ufm_slot"].append(lat_ufm_slot[tw]) woff[ttyp] += 1 break off += 1 tb["toast_hex_pos"] = ltube_toasthex_pos[tindx] tb["optics_pos"] = ltube_optics_pos[tindx] tb["tube_name"] = "" tb["receiver_name"] = "" tube_slots[nm] = tb # These are taken from: # https://simonsobs.atlassian.net/wiki/spaces/PRO/pages/101974017/Focal+Plane+Coordinates#UFM-Layout.1 hex_to_ufm_slot = [ 0, 2, 1, 6, 5, 4, 3, ] hex_to_ufm_loc = [ "AX", "NE", "NO", "NW", "SW", "SO", "SE", ] hex_to_ufm_thetarot = [ 240.0, 0.0, 300.0, 180.0, 120.0, 60.0, 60.0, ] stubes = ["SAT_MF", "SAT_MF", "SAT_UHF", "SAT_LF"] for tindx in range(4): nm = "ST{:d}".format(tindx+1) ttyp = stubes[tindx] tb = OrderedDict() tb["type"] = ttyp tb["waferspace"] = 128.4 tb["wafer_slots"] = list() tb["wafer_slot_angle"] = [ hex_to_ufm_thetarot[tw] for tw in range(7) ] # Degrees # The "slot" here is the relative slot (ws0 - ws6) within the tube. tb["wafer_ufm_slot"] = list() # The "loc" here is the compass direction name (e.g. NO, NE, SW, etc.) tb["wafer_ufm_loc"] = list() for tw in range(7): off = 0 for w, props in cnf["wafer_slots"].items(): if props["type"] == ttyp: if off == woff[ttyp]: props["tube_index"] = tw tb["wafer_slots"].append(w) tb["wafer_ufm_slot"].append(hex_to_ufm_slot[tw]) tb["wafer_ufm_loc"].append(hex_to_ufm_loc[tw]) woff[ttyp] += 1 break off += 1 tb["toast_hex_pos"] = 0 tb["optics_pos"] = 0 tb["tube_name"] = "" tb["receiver_name"] = "" tube_slots[nm] = tb cnf["tube_slots"] = tube_slots telescopes = OrderedDict() tele = OrderedDict() tele["tube_slots"] = ["c1", "i5", "i6", "i1", "i3", "i4", "o6"] tele["platescale"] = 0.00495 # This tube spacing in mm corresponds to 1.78 degrees projected on # the sky at a plate scale of 0.00495 deg/mm. tele["tubespace"] = 359.6 fwhm = OrderedDict() fwhm["LAT_f030"] = 7.4 fwhm["LAT_f040"] = 5.1 fwhm["LAT_f090"] = 2.2 fwhm["LAT_f150"] = 1.4 fwhm["LAT_f230"] = 1.0 fwhm["LAT_f290"] = 0.9 tele["fwhm"] = fwhm tele["platform_name"] = "" telescopes["LAT"] = tele fwhm_sat = OrderedDict() fwhm_sat["SAT_f030"] = 91.0 fwhm_sat["SAT_f040"] = 63.0 fwhm_sat["SAT_f090"] = 30.0 fwhm_sat["SAT_f150"] = 17.0 fwhm_sat["SAT_f230"] = 11.0 fwhm_sat["SAT_f290"] = 9.0 tele = OrderedDict() tele["tube_slots"] = ["ST1"] tele["platescale"] = 0.09668 tele["fwhm"] = fwhm_sat tele["platform_name"] = "" telescopes["SAT1"] = tele tele = OrderedDict() tele["tube_slots"] = ["ST2"] tele["platescale"] = 0.09668 tele["fwhm"] = fwhm_sat tele["platform_name"] = "" telescopes["SAT2"] = tele tele = OrderedDict() tele["tube_slots"] = ["ST3"] tele["platescale"] = 0.09668 tele["fwhm"] = fwhm_sat tele["platform_name"] = "" telescopes["SAT3"] = tele tele = OrderedDict() tele["tube_slots"] = ["ST4"] tele["platescale"] = 0.09668 tele["fwhm"] = fwhm_sat tele["platform_name"] = "" telescopes["SAT4"] = tele cnf["telescopes"] = telescopes card_slots = OrderedDict() crate_slots = OrderedDict() crt_indx = 0 for tel in cnf["telescopes"]: crn = "crate_slot{:02d}".format(crt_indx) crt = OrderedDict() crt["card_slots"] = list() crt["telescope"] = tel crt["crate_name"] = "" ## get all the wafer card numbers for a telescope tb_wfrs = [cnf["tube_slots"][t]["wafer_slots"] for t in cnf["telescopes"][tel]["tube_slots"]] tl_wfrs = [i for sl in tb_wfrs for i in sl] wafer_cards = [cnf["wafer_slots"][w]["card_slot"] for w in tl_wfrs] # add all cards to the card table and assign to crates for crd in wafer_cards: cdprops = OrderedDict() cdprops["nbias"] = 12 cdprops["nAMC"] = 2 cdprops["nchannel"] = 1764 cdprops["card_name"] = "" card_slots[crd] = cdprops crt["card_slots"].append(crd) # name new crates when current one is full if ('S' in tel and len(crt["card_slots"]) >=4) or len(crt["card_slots"]) >=6: crate_slots[crn] = crt crt_indx += 1 crn = "crate_slot{:02d}".format(crt_indx) crt = OrderedDict() crt["card_slots"] = list() crt["telescope"] = tel crt["crate_name"] = "" # each telescope starts with a new crate crate_slots[crn] = crt crt_indx += 1 cnf["card_slots"] = card_slots cnf["crate_slots"] = crate_slots # Add an empty set of detectors here, in case the user just wants access to # the hardware metadata. cnf["detectors"] = OrderedDict() hw = Hardware() hw.data = cnf return hw
def telescope_tube_wafer(): """Global mapping of telescopes, tubes, and wafers used in simulations. This mapping is here rather than core.hardware, so that we could put alternate definitions there for actual fielded configurations. Returns: (dict): The mapping """ hw = sim_nominal() result = dict() for tele_name, tele_props in hw.data["telescopes"].items(): tb = dict() for tube_name in tele_props["tube_slots"]: tube_props = hw.data["tube_slots"][tube_name] tb[tube_name] = list(tube_props["wafer_slots"]) result[tele_name] = tb return result