diff --git a/proseco/acq.py b/proseco/acq.py index f4084741..553e42f5 100644 --- a/proseco/acq.py +++ b/proseco/acq.py @@ -126,6 +126,12 @@ class AcqTable(ACACatalogTable): # Required attributes required_attrs = ('att', 'man_angle', 't_ccd_acq', 'date', 'dither_acq') + t_ccd = AliasAttribute() # Maps t_ccd to t_ccd_acq base attribute + dither = AliasAttribute() # .. and likewise. + include_ids = AliasAttribute() + include_halfws = AliasAttribute() + exclude_ids = AliasAttribute() + p_man_errs = MetaAttribute(is_kwarg=False) cand_acqs = MetaAttribute(is_kwarg=False) p_safe = MetaAttribute(is_kwarg=False) @@ -174,11 +180,6 @@ def empty(cls): out['halfw'] = np.full(fill_value=0, shape=(0,), dtype=np.int64) return out - t_ccd = AliasAttribute() - dither = AliasAttribute() - include_ids = AliasAttribute() - include_halfws = AliasAttribute() - exclude_ids = AliasAttribute() @property def fid_set(self): diff --git a/proseco/catalog.py b/proseco/catalog.py index f913c1f4..ede42dab 100644 --- a/proseco/catalog.py +++ b/proseco/catalog.py @@ -9,6 +9,10 @@ from .acq import get_acq_catalog, AcqTable from .fid import get_fid_catalog, FidTable from . import characteristics_acq as ACQ +from . import characteristics as ACA +from . import test as test_from_init + +VERSION = test_from_init(get_version=True) def get_aca_catalog(obsid=0, **kwargs): @@ -96,6 +100,14 @@ def _get_aca_catalog(**kwargs): aca.call_args = kwargs.copy() aca.set_attrs_from_kwargs(**kwargs) + # Override t_ccd related inputs with effective temperatures for downstream + # action by AcqTable, GuideTable, FidTable. See set_attrs_from_kwargs() + # method for more details. + if 't_ccd' in kwargs: + del kwargs['t_ccd'] + kwargs['t_ccd_acq'] = aca.t_ccd_eff_acq + kwargs['t_ccd_guide'] = aca.t_ccd_eff_guide + # Get stars (typically from AGASC) and do not filter for stars near # the ACA FOV. This leaves the full radial selection available for # later roll optimization. Use aca.stars or aca.acqs.stars from here. @@ -144,6 +156,22 @@ def _get_aca_catalog(**kwargs): return aca +def get_effective_t_ccd(t_ccd): + """Return the effective T_ccd used for selection and catalog evaluation. + + For details see Dynamic ACA limits in baby steps section in: + https://occweb.cfa.harvard.edu/twiki/bin/view/Aspect/StarWorkingGroupMeeting2019x02x13 + + :param t_ccd: + :return: + """ + t_limit = ACA.aca_t_ccd_planning_limit + if t_ccd > t_limit: + return t_ccd + 1 + (t_ccd - t_limit) + else: + return t_ccd + + class ACATable(ACACatalogTable): """Container ACACatalogTable class that has merged guide / acq / fid catalogs as attributes and other methods relevant to the merged catalog. @@ -151,11 +179,28 @@ class ACATable(ACACatalogTable): """ optimize = MetaAttribute(default=True) call_args = MetaAttribute(default={}) + version = MetaAttribute() # For validation with get_aca_catalog(obsid), store the starcheck # catalog in the ACATable meta. starcheck_catalog = MetaAttribute(is_kwarg=False) + # Effective T_ccd used for dynamic ACA limits (see updates_for_t_ccd_effective() + # method below). + t_ccd_eff_acq = MetaAttribute(is_kwarg=False) + t_ccd_eff_guide = MetaAttribute(is_kwarg=False) + + @property + def t_ccd(self): + # For top-level ACATable object use the guide temperature, which is always + # greater than or equal to the acq temperature. + return self.t_ccd_guide + + @t_ccd.setter + def t_ccd(self, value): + self.t_ccd_guide = value + self.t_ccd_acq = value + @classmethod def empty(cls): out = super().empty() @@ -170,6 +215,31 @@ def thumbs_up(self): self.fids.thumbs_up & self.guides.thumbs_up) + def set_attrs_from_kwargs(self, **kwargs): + """Set object attributes from kwargs. + + After calling the base class method which does all the real work, then + compute the effective T_ccd temperatures. + + In this ACATable object: + - t_ccd_eff_{acq,guide} are the effective T_ccd values which are adjusted + if the actual t_ccd{acq,guide} values are above ACA.aca_t_ccd_planning_limit. + - t_ccd_{acq,guide} are the actual (or predicted) values from the call + + The downstream AcqTable, GuideTable, and FidTable are initialized with the + *effective* values as t_ccd. Those classes do not have the concept of effective + temperature. + + :param kwargs: dict of input kwargs + :return: dict + """ + super().set_attrs_from_kwargs(**kwargs) + + self.t_ccd_eff_acq = get_effective_t_ccd(self.t_ccd_acq) + self.t_ccd_eff_guide = get_effective_t_ccd(self.t_ccd_guide) + self.version = VERSION + + def get_review_table(self): """Get ACAReviewTable object based on self. diff --git a/proseco/characteristics.py b/proseco/characteristics.py index efe8112f..a477da6c 100644 --- a/proseco/characteristics.py +++ b/proseco/characteristics.py @@ -25,6 +25,9 @@ col_spoiler_mag_diff = 4.5 col_spoiler_pix_sep = 10 # pixels +# ACA T_ccd planning limit (degC) +aca_t_ccd_planning_limit = -9.5 + # Dark current that corresponds to a 5.0 mag star in a single pixel. Apply # this value to the region specified by bad_pixels. bad_pixel_dark_current = 700_000 diff --git a/proseco/core.py b/proseco/core.py index 9d0d54f0..4ac048bb 100644 --- a/proseco/core.py +++ b/proseco/core.py @@ -692,11 +692,12 @@ def dither(self, value): @property def t_ccd(self): - return self.t_ccd_guide + # Subclasses must implement. + raise NotImplementedError() @t_ccd.setter def t_ccd(self, value): - self.t_ccd_guide = value + raise NotImplementedError() @classmethod def empty(cls): diff --git a/proseco/fid.py b/proseco/fid.py index 8db51cd7..d7360f48 100644 --- a/proseco/fid.py +++ b/proseco/fid.py @@ -107,6 +107,15 @@ def acqs(self, val): """ self._acqs = weakref.ref(val) + @property + def t_ccd(self): + # For fids use the guide CCD temperature + return self.t_ccd_guide + + @t_ccd.setter + def t_ccd(self, value): + self.t_ccd_guide = value + @property def thumbs_up(self): if self.n_fid == 0: diff --git a/proseco/guide.py b/proseco/guide.py index 15f371c7..874a51e9 100644 --- a/proseco/guide.py +++ b/proseco/guide.py @@ -94,8 +94,8 @@ def reject(self, reject): reject_info = self.reject_info reject_info.append(reject) - t_ccd = AliasAttribute() - dither = AliasAttribute() + t_ccd = AliasAttribute() # Maps t_ccd to t_ccd_guide attribute from base class + dither = AliasAttribute() # .. and likewise. include_ids = AliasAttribute() exclude_ids = AliasAttribute() diff --git a/proseco/tests/test_catalog.py b/proseco/tests/test_catalog.py index 2243d8b0..cb3ca5ce 100644 --- a/proseco/tests/test_catalog.py +++ b/proseco/tests/test_catalog.py @@ -2,6 +2,7 @@ import copy import matplotlib + matplotlib.use('agg') import pickle @@ -12,12 +13,11 @@ import mica.starcheck from .test_common import STD_INFO, mod_std_info, DARK40, OBS_INFO -from ..core import StarsTable, ACACatalogTable -from ..catalog import get_aca_catalog +from ..core import StarsTable +from ..catalog import get_aca_catalog, ACATable from ..fid import get_fid_catalog from .. import characteristics as ACA - HAS_SC_ARCHIVE = Path(mica.starcheck.starcheck.FILES['data_root']).exists() TEST_COLS = 'slot idx id type sz yang zang dim res halfw'.split() @@ -57,7 +57,6 @@ def test_get_aca_catalog_20603(): ' 7 9 116791744 ACQ 6x6 985.38 -1210.19 20 1 140', ' 0 10 40108048 ACQ 6x6 2.21 1619.17 20 1 140'] - repr(aca) # Apply default formats assert aca[TEST_COLS].pformat(max_width=-1) == exp @@ -168,7 +167,7 @@ def test_big_dither_from_mica_starcheck(): Test code that infers dither_acq and dither_guide for a big-dither observation like 20168. """ - aca = ACACatalogTable() + aca = ACATable() aca.set_attrs_from_kwargs(obsid=20168) assert aca.detector == 'HRC-S' @@ -229,6 +228,7 @@ def test_pickle(): atol=0, rtol=1e-6) assert aca.acqs.fid_set == aca2.acqs.fid_set + def test_copy_deepcopy_pickle(): """ Test that copy, deepcopy and pickle all return the expected object which @@ -280,6 +280,88 @@ def test_big_sim_offset(): assert all(name in aca.fids.colnames for name in names) +@pytest.mark.parametrize('call_t_ccd', [True, False]) +def test_calling_with_t_ccd_acq_guide(call_t_ccd): + """Test that calling get_aca_catalog with t_ccd or t_ccd_acq/guide args sets all + CCD attributes correctly in the nominal case of a temperature + below the planning limit. + + """ + dark = DARK40.copy() + stars = StarsTable.empty() + stars.add_fake_constellation(mag=8.0, n_stars=8) + + t_ccd = np.trunc(ACA.aca_t_ccd_planning_limit - 1.0) + + if call_t_ccd: + # Call with just t_ccd=t_ccd + t_ccd_guide = t_ccd + t_ccd_acq = t_ccd + ccd_kwargs = {'t_ccd': t_ccd} + else: + # Call with separate values + t_ccd_guide = t_ccd + t_ccd_acq = t_ccd - 1 + ccd_kwargs = {'t_ccd_acq': t_ccd_acq, 't_ccd_guide': t_ccd_guide} + + kwargs = mod_std_info(stars=stars, dark=dark, **ccd_kwargs) + aca = get_aca_catalog(**kwargs) + + assert aca.t_ccd == t_ccd_guide + assert aca.t_ccd_acq == t_ccd_acq + assert aca.t_ccd_guide == t_ccd_guide + + assert aca.t_ccd_eff_acq == t_ccd_acq + assert aca.t_ccd_eff_guide == t_ccd_guide + + assert aca.acqs.t_ccd == t_ccd_acq + assert aca.acqs.t_ccd_acq == t_ccd_acq + assert aca.acqs.t_ccd_guide == t_ccd_guide + + assert aca.guides.t_ccd == t_ccd_guide + assert aca.guides.t_ccd_guide == t_ccd_guide + assert aca.guides.t_ccd_acq == t_ccd_acq + + assert aca.fids.t_ccd == t_ccd_guide + assert aca.fids.t_ccd_guide == t_ccd_guide + assert aca.fids.t_ccd_acq == t_ccd_acq + + +t_ccd_cases = [(-0.5, 0, 0), + (0, 0, 0), + (0.5, 1.5, 1.4)] + + +@pytest.mark.parametrize('t_ccd_case', t_ccd_cases) +def test_t_ccd_effective_acq_guide(t_ccd_case): + """Test setting of effective T_ccd temperatures for cases above and + below the planning limit. + + """ + stars = StarsTable.empty() + stars.add_fake_constellation(mag=8.0, n_stars=8) + + t_limit = ACA.aca_t_ccd_planning_limit + + t_offset, t_penalty_acq, t_penalty_guide = t_ccd_case + # Set acq and guide temperatures different + t_ccd_acq = t_limit + t_offset + t_ccd_guide = t_ccd_acq - 0.1 + + kwargs = mod_std_info(stars=stars, t_ccd_acq=t_ccd_acq, t_ccd_guide=t_ccd_guide) + aca = get_aca_catalog(**kwargs) + + assert np.isclose(aca.t_ccd_acq, t_ccd_acq) + assert np.isclose(aca.t_ccd_guide, t_ccd_guide) + + # t_ccd + 1 + (t_ccd - t_limit) from proseco.catalog.get_effective_t_ccd() + assert np.isclose(aca.t_ccd_eff_acq, t_ccd_acq + t_penalty_acq) + assert np.isclose(aca.t_ccd_eff_guide, t_ccd_guide + t_penalty_guide) + + assert np.isclose(aca.t_ccd_eff_acq, aca.acqs.t_ccd) + assert np.isclose(aca.t_ccd_eff_guide, aca.guides.t_ccd) + + def test_call_args_attr(): aca = get_aca_catalog(**mod_std_info(optimize=False, n_guide=0, n_acq=0, n_fid=0)) assert aca.call_args == {'att': (0, 0, 0), @@ -540,7 +622,7 @@ def test_report_from_objects(tmpdir): obsdir = rootdir / f'obs{obsid:05}' for subdir in 'acq', 'guide': outdir = obsdir / subdir - assert(outdir / 'index.html').exists() + assert (outdir / 'index.html').exists() assert len(list(outdir.glob('*.png'))) > 0