"""
desitarget.targets
==================
Presumably this defines targets.
.. _`DocDB 2348`: https://desi.lbl.gov/DocDB/cgi-bin/private/RetrieveFile?docid=2348
"""
import numpy as np
import healpy as hp
import numpy.lib.recfunctions as rfn
from importlib import import_module
from astropy.table import Table
from desitarget.targetmask import desi_mask, bgs_mask, mws_mask
from desitarget.targetmask import scnd_mask, targetid_mask
from desitarget.targetmask import obsconditions
# ADM set up the DESI default logger.
from desiutil.log import get_logger
log = get_logger()
# ADM common redshift that defines a Lyman-Alpha QSO.
zcut = 2.1
# ADM common redshift that defines a QSO to be reobserved for
# ADM the Gontcho a Gontcho and Weiner et al. secondary programs.
midzcut = 1.6
[docs]def encode_targetid(objid=None, brickid=None, release=None,
mock=None, sky=None, gaiadr=None):
"""Create the DESI TARGETID from input source and imaging info.
Parameters
----------
objid : :class:`int` or :class:`~numpy.ndarray`, optional
The OBJID from Legacy Surveys imaging or the row within
a Gaia HEALPixel file in $GAIA_DIR/healpix if
`gaia` is not ``None``.
brickid : :class:`int` or :class:`~numpy.ndarray`, optional
The BRICKID from Legacy Surveys imaging.
or the Gaia HEALPixel chunk number for files in
$GAIA_DIR/healpix if `gaia` is not ``None``.
release : :class:`int` or :class:`~numpy.ndarray`, optional
The RELEASE from Legacy Surveys imaging. Or, if < 1000,
the secondary target class bit flag number from
'data/targetmask.yaml'. Or, if < 1000 and `sky` is not
``None``, the HEALPixel processing number for SUPP_SKIES.
mock : :class:`int` or :class:`~numpy.ndarray`, optional
1 if this object is a mock object (generated from mocks or from
a random catalog, not from real survey data), 0 otherwise
sky : :class:`int` or :class:`~numpy.ndarray`, optional
1 if this object is a blank sky object, 0 otherwise
gaiadr : :class:`int` or :class:`~numpy.ndarray`, optional
The Gaia Data Release number (e.g. send 2 for Gaia DR2).
A value of 1 does NOT mean DR1. Rather it has the specific
meaning of a DESI first-light commissioning target.
Returns
-------
:class:`int` or `~numpy.ndarray`
The TARGETID for DESI, encoded according to the bits listed in
:meth:`desitarget.targetid_mask`. If an integer is passed, then
an integer is returned, otherwise an array is returned.
Notes
-----
- Has maximum flexibility so that mixes of integers and arrays
can be passed, in case some value like BRICKID or SKY
is the same for a set of objects. Consider, e.g.:
print(
targets.decode_targetid(
targets.encode_targetid(objid=np.array([234,12]),
brickid=np.array([234,12]),
release=4000,
sky=[1,0]))
)
(array([234,12]), array([234,12]), array([4000,4000]),
array([0,0]), array([1,0]), array([0, 0]))
- See also `DocDB 2348`_.
"""
# ADM a flag that tracks whether the main inputs were integers.
intpassed = True
# ADM the names of the bits with RESERVED removed.
bitnames = targetid_mask.names()
if "RESERVED" in bitnames:
bitnames.remove("RESERVED")
# ADM determine the length of passed values that aren't None.
# ADM default to an integer (length 1).
nobjs = 1
inputs = [objid, brickid, release, mock, sky, gaiadr]
goodpar = [param is not None for param in inputs]
firstgoodpar = np.where(goodpar)[0][0]
if isinstance(inputs[firstgoodpar], np.ndarray):
nobjs = len(inputs[firstgoodpar])
intpassed = False
# ADM set parameters that weren't passed to zerod arrays
# ADM set integers that were passed to at least 1D arrays
for i, param in enumerate(inputs):
if param is None:
inputs[i] = np.zeros(nobjs, dtype='int64')
else:
inputs[i] = np.atleast_1d(param)
# ADM check passed parameters don't exceed their bit-allowance
# ADM and aren't negative numbers.
for param, bitname in zip(inputs, bitnames):
msg = 'Invalid range when making targetid: {} '.format(bitname)
if not np.all(param < 2**targetid_mask[bitname].nbits):
msg += 'cannot exceed {}'.format(2**targetid_mask[bitname].nbits - 1)
if not np.all(param >= 0):
msg += 'cannot be negative'
if 'cannot' in msg:
log.critical(msg)
raise IOError(msg)
# ADM set up targetid as an array of 64-bit integers.
targetid = np.zeros(nobjs, ('int64'))
# ADM populate TARGETID. Shift to type integer 64 to avoid casting.
for param, bitname in zip(inputs, bitnames):
targetid |= param.astype('int64') << targetid_mask[bitname].bitnum
# ADM if the main inputs were integers, return an integer.
if intpassed:
return targetid[0]
return targetid
[docs]def decode_targetid(targetid):
"""break a DESI TARGETID into its constituent parts.
Parameters
----------
:class:`int` or :class:`~numpy.ndarray`
The TARGETID for DESI, encoded according to the bits listed in
:meth:`desitarget.targetid_mask`.
Returns
-------
:class:`int` or :class:`~numpy.ndarray`
The OBJID from Legacy Surveys imaging or the row within
a Gaia HEALPixel file in $GAIA_DIR/healpix if
`gaia` is not ``None``.
:class:`int` or :class:`~numpy.ndarray`
The BRICKID from Legacy Surveys imaging.
or the Gaia HEALPixel chunk number for files in
$GAIA_DIR/healpix if `gaia` is not ``None``.
:class:`int` or :class:`~numpy.ndarray`
The RELEASE from Legacy Surveys imaging. Or, if < 1000,
the secondary target class bit flag number from
'data/targetmask.yaml'. Or, if < 1000 and `sky` is not
``None``, the HEALPixel processing number for SUPP_SKIES.
:class:`int` or :class:`~numpy.ndarray`
1 if this object is a mock object (generated from mocks or from
a random catalog, not from real survey data), 0 otherwise
:class:`int` or :class:`~numpy.ndarray`
1 if this object is a blank sky object, 0 otherwise
:class:`int` or :class:`~numpy.ndarray`
The Gaia Data Release number (e.g. will be 2 for Gaia DR2).
A value of 1 does NOT mean DR1. Rather it has the specific
meaning of a DESI first-light commissioning target.
Notes
-----
- if a 1-D array is passed, then an integer is returned.
Otherwise an array is returned.
- see also `DocDB 2348`_.
"""
# ADM the names of the bits with RESERVED removed.
bitnames = targetid_mask.names()
if "RESERVED" in bitnames:
bitnames.remove("RESERVED")
# ADM retrieve each value by left-shifting by the number of bits
# ADM that comprise the value, to the left-end of the value, and
# ADM then right-shifting to the right-end.
outputs = []
for bitname in bitnames:
bitnum = targetid_mask[bitname].bitnum
val = (targetid & (2**targetid_mask[bitname].nbits - 1
<< targetid_mask[bitname].bitnum)) >> bitnum
outputs.append(val)
return outputs
[docs]def encode_negative_targetid(ra, dec, group=1):
"""
Create negative 64-bit TARGETID from (ra,dec) unique to ~1.2 milliarcsec
Parameters
----------
ra : :class:`float` or :class:`~numpy.ndarray`
Right Ascension in degrees 0 <= ra <= 360
dec : :class:`float` or :class:`~numpy.ndarray`
Declination in degrees -90 <= dec <= 90
group : int, optional (default 1)
group number 1-15 to encode
Returns
-------
:class:`~numpy.int64` or :class:`~numpy.ndarray`
negative TARGETID derived from (ra,dec)
"""
# Hardcode number of bits.
nbits_ra = 30
nbits_dec = 29
nbits_group = 4
# Check input dimensionality.
scalar_input = np.isscalar(ra)
if np.isscalar(ra) != np.isscalar(dec):
raise TypeError('ra and dec must both be scalars or both be arrays')
if not (1 <= group <= 15):
raise ValueError(f'group {group} must be within 1-15')
group = np.int8(group)
# Convert to arrays to enable things like .astype(int).
ra = np.atleast_1d(ra)
dec = np.atleast_1d(dec)
assert np.all((0.0 <= ra) & (ra <= 360.0))
assert np.all((-90.0 <= dec) & (dec <= 90.0))
# encode ra in bits 30-59 and dec in bits 0-29.
ra_bits = ((2**nbits_ra - 1) * (ra/360.0)).astype(int)
dec_bits = ((2**nbits_dec - 1) * ((dec+90.0)/180.0)).astype(int)
group_bitshift = nbits_dec + nbits_ra
ra_bitshift = nbits_dec
targetid = -((group << group_bitshift) + (ra_bits << ra_bitshift) + dec_bits)
# return value has dimensionality of inputs.
if scalar_input:
return targetid[0]
else:
return targetid
[docs]def decode_negative_targetid(targetid):
"""
TODO: document
"""
# Hardcode number of bits.
nbits_ra = 30
nbits_dec = 29
nbits_group = 4
dec_mask = 2**nbits_dec - 1
ra_mask = 2**nbits_ra - 1
group_mask = 2**nbits_group - 1
group_bitshift = nbits_dec + nbits_ra
ra_bitshift = nbits_dec
dec_bits = (-targetid) & dec_mask
ra_bits = ((-targetid) >> ra_bitshift) & ra_mask
group = ((-targetid) >> group_bitshift) & group_mask
ra = ra_bits / (2**nbits_ra - 1) * 360.0
dec = dec_bits / (2**nbits_dec - 1) * 180.0 - 90.0
return ra, dec, group
[docs]def switch_main_cmx_or_sv(revamp, archetype):
"""change the data model of a set of targets to match another.
Parameters
----------
revamp : :class:`~numpy.ndarray`
An array of targets generated by, e.g., :mod:`~desitarget.cuts`
must include columns `DESI_TARGET`, `MWS_TARGET` and `BGS_TARGET`
or the corresponding commissioning or SV columns.
archetype : :class:`~numpy.ndarray`
Like `revamp` but with a different flavor of `DESI_TARGET`,
`MWS_TARGET` and `BGS_TARGET` columns. For instance, `revamp`
might have the Main Survey columns and `archetype` might have the
SV1 columns.
Returns
-------
:class:`~numpy.ndarray`
`revamp` but with the flavor of `DESI_TARGET`, `MWS_TARGET` and
`BGS_TARGET` updated to match that of `archetype`
"""
# ADM change the SCND_TARGET-like column too, if it exists.
scnd = np.any(["SCND_TARGET" in i for i in revamp.dtype.names])
# ADM what are the column names in the file to be changed?
oldcols, _, _ = main_cmx_or_sv(revamp, scnd=scnd)
# ADM what are the column names to change to?
newcols, _, _ = main_cmx_or_sv(archetype, scnd=scnd)
# ADM update the column names.
renamer = {oldcol: newcol for oldcol, newcol in zip(oldcols, newcols)}
renamed = rfn.rename_fields(revamp, renamer)
# ADM guard against commissioning files.
if "CMX_TARGET" in newcols:
renamed = rfn.drop_fields(renamed, oldcols)
return renamed
[docs]def main_cmx_or_sv(targets, rename=False, scnd=False):
"""whether a target array is main survey, commissioning, or SV.
Parameters
----------
targets : :class:`~numpy.ndarray`
An array of targets generated by, e.g., :mod:`~desitarget.cuts`
must include at least (all of) the columns `DESI_TARGET`, `MWS_TARGET` and
`BGS_TARGET` or the corresponding commissioning or SV columns.
rename : :class:`bool`, optional, defaults to ``False``
If ``True`` then also return a copy of `targets` with the input `_TARGET`
columns renamed to reflect the main survey format.
scnd : :class:`bool`, optional, defaults to ``False``
If ``True``, add the secondary target information to the output.
Returns
-------
:class:`list`
A list of strings corresponding to the target columns names. For the main survey
this would be [`DESI_TARGET`, `BGS_TARGET`, `MWS_TARGET`], for commissioning it
would just be [`CMX_TARGET`], for SV1 it would be [`SV1_DESI_TARGET`,
`SV1_BGS_TARGET`, `SV1_MWS_TARGET`]. Also includes, e.g. `SCND_TARGET`, if
`scnd` is passed as ``True``.
:class:`list`
A list of the masks that correspond to each column from the relevant main/cmx/sv
yaml file. Also includes the relevant SCND_MASK, if `scnd` is passed as True.
:class:`str`
The string 'main', 'cmx' or 'svX' (where X = 1, 2, 3 etc.) for the main survey,
commissioning and an iteration of SV. Specifies which type of file was sent.
:class:`~numpy.ndarray`, optional, if `rename` is ``True``
A copy of the input targets array with the `_TARGET` columns renamed to
`DESI_TARGET`, and (if they exist) `BGS_TARGET`, `MWS_TARGET`.
"""
# ADM default to the main survey.
maincolnames = ["DESI_TARGET", "BGS_TARGET", "MWS_TARGET", "SCND_TARGET"]
outcolnames = maincolnames.copy()
masks = [desi_mask, bgs_mask, mws_mask, scnd_mask]
survey = 'main'
# ADM set survey to correspond to commissioning or SV if those columns exist
# ADM and extract the column names of interest.
incolnames = np.array(targets.dtype.names)
notmain = np.array(['SV' in name or 'CMX' in name for name in incolnames])
if np.any(notmain):
outcolnames = list(incolnames[notmain])
survey = outcolnames[0].split('_')[0].lower()
if survey[:2] == 'sv':
outcolnames = ["{}_{}".format(survey.upper(), col) for col in maincolnames]
# ADM retrieve the correct masks, depending on the survey type.
if survey == 'cmx':
from desitarget.cmx.cmx_targetmask import cmx_mask
masks = [cmx_mask]
elif survey[:2] == 'sv':
try:
targmask = import_module("desitarget.{}.{}_targetmask".format(
survey, survey))
except ModuleNotFoundError:
msg = 'Bitmask yaml does not exist for survey type {}'.format(survey)
log.critical(msg)
raise ModuleNotFoundError(msg)
masks = [targmask.desi_mask, targmask.bgs_mask,
targmask.mws_mask, targmask.scnd_mask]
elif survey != 'main':
msg = "input target file must be 'main', 'cmx' or 'sv', not {}!!!".format(survey)
log.critical(msg)
raise ValueError(msg)
if not scnd:
outcolnames = outcolnames[:3]
masks = masks[:3]
# ADM if requested, rename the columns.
if rename:
mapper = {}
for i, col in enumerate(outcolnames):
mapper[col] = maincolnames[i]
return outcolnames, masks, survey, rfn.rename_fields(targets, mapper)
return outcolnames, masks, survey
[docs]def set_obsconditions(targets, scnd=False):
"""set the OBSCONDITIONS mask for each target bit.
Parameters
----------
targets : :class:`~numpy.ndarray`
An array of targets generated by, e.g., :mod:`~desitarget.cuts`.
Must include at least (all of) the columns `DESI_TARGET`,
`BGS_TARGET`, `MWS_TARGET` or corresponding cmx or SV columns.
scnd : :class:`bool`, optional, defaults to ``False``
If ``True`` then make all of the comparisons on the `SCND_TARGET`
column instead of `DESI_TARGET`, `BGS_TARGET` and `MWS_TARGET`.
Returns
-------
:class:`~numpy.ndarray`
The OBSCONDITIONS bitmask for the passed targets.
Notes
-----
- the OBSCONDITIONS for each target bit is in the file, e.g.
data/targetmask.yaml. It can be retrieved using, for example,
`obsconditions.mask(desi_mask["ELG"].obsconditions)`.
"""
colnames, masks, _ = main_cmx_or_sv(targets, scnd=scnd)
# ADM if we requested secondary targets, the needed information
# ADM was returned as the last part of each array.
if scnd:
colnames, masks = colnames[-1:], masks[-1:]
n = len(targets)
from desitarget.mtl import mtldatamodel as mtldm
obscon = np.zeros(n, dtype=mtldm["OBSCONDITIONS"].dtype)
for mask, xxx_target in zip(masks, colnames):
for name in mask.names():
# ADM which targets have this bit for this mask set?
ii = (targets[xxx_target] & mask[name]) != 0
# ADM under what conditions can that bit be observed?
if np.any(ii):
obscon[ii] |= obsconditions.mask(mask[name].obsconditions)
return obscon
[docs]def initial_priority_numobs(targets, scnd=False,
obscon="DARK|GRAY|BRIGHT|BACKUP|TWILIGHT12|TWILIGHT18"):
"""highest initial priority and numobs for an array of target bits.
Parameters
----------
targets : :class:`~numpy.ndarray`
An array of targets generated by, e.g., :mod:`~desitarget.cuts`.
Must include at least (all of) the columns `DESI_TARGET`,
`BGS_TARGET`, `MWS_TARGET` or corresponding cmx or SV columns.
scnd : :class:`bool`, optional, defaults to ``False``
If ``True`` then make all of the comparisons on the `SCND_TARGET`
column instead of `DESI_TARGET`, `BGS_TARGET` and `MWS_TARGET`.
obscon : :class:`str`, optional, defaults to almost all OBSCONDITIONS
A combination of strings that are in the desitarget bitmask yaml
file (specifically in `desitarget.targetmask.obsconditions`).
Returns
-------
:class:`~numpy.ndarray`
An array of integers corresponding to the highest initial
priority for each target consistent with the constraints
on observational conditions imposed by `obscon`.
:class:`~numpy.ndarray`
An array of integers corresponding to the largest number of
observations for each target consistent with the constraints
on observational conditions imposed by `obscon`.
Notes
-----
- the initial priority for each target bit is in the file, e.g.,
data/targetmask.yaml. It can be retrieved using, for example,
`desi_mask["ELG"].priorities["UNOBS"]`.
- the input obscon string can be converted to a bitmask using
`desitarget.targetmask.obsconditions.mask(blat)`.
"""
colnames, masks, _ = main_cmx_or_sv(targets, scnd=scnd)
# ADM if we requested secondary targets, the needed information
# ADM was returned as the last part of each array.
if scnd:
colnames, masks = colnames[-1:], masks[-1:]
# ADM set up the output arrays. Remember calibs have NUMOBS of -1.
# ADM Such calibs will be passed over as they don't have UNOBS set.
outpriority = np.zeros(len(targets), dtype='int')-1
outnumobs = np.zeros(len(targets), dtype='int')-1
# ADM convert the passed obscon string to bits.
obsbits = obsconditions.mask(obscon)
# ADM loop through the masks to establish all bitnames of interest.
for colname, mask in zip(colnames, masks):
# ADM first determine which bits actually have priorities.
bitnames = []
for name in mask.names():
try:
_ = mask[name].priorities["UNOBS"]
# ADM also only consider bits with correct OBSCONDITIONS.
obsforname = obsconditions.mask(mask[name].obsconditions)
if (obsforname & obsbits) != 0:
bitnames.append(name)
except KeyError:
pass
# ADM loop through the relevant bits updating with the highest
# ADM priority and the largest value of NUMOBS.
for name in bitnames:
# ADM indexes in the DESI/MWS/BGS_TARGET column that have this bit set
istarget = (targets[colname] & mask[name]) != 0
# ADM for each index, determine where this bit is set and the priority
# ADM for this bit is > than the currently stored priority.
w = np.where((mask[name].priorities['UNOBS'] >= outpriority) & istarget)[0]
# ADM where a larger priority trumps the stored priority, update the priority
if len(w) > 0:
outpriority[w] = mask[name].priorities['UNOBS']
# ADM for each index, determine where this bit is set and whether NUMOBS
# ADM for this bit is > than the currently stored NUMOBS.
w = np.where((mask[name].numobs >= outnumobs) & istarget)[0]
# ADM where a larger NUMOBS trumps the stored NUMOBS, update NUMOBS.
if len(w) > 0:
outnumobs[w] = mask[name].numobs
return outpriority, outnumobs
[docs]def calc_numobs_more(targets, zcat, obscon):
"""
Calculate target NUMOBS_MORE from masks, observation/redshift status.
Parameters
----------
targets : :class:`~numpy.ndarray`
numpy structured array or astropy Table of targets. Must include
the columns `DESI_TARGET`, `BGS_TARGET`, `MWS_TARGET` (or their
SV/cmx equivalents) `TARGETID` and `NUMOBS_INIT`. For Main Survey
targets, must also contain `PRIORITY` if this isn't the first
time through MTL (used to "lock in" the state of Ly-Alpha QSOs).
zcat : :class:`~numpy.ndarray`
numpy structured array or Table of redshift info. Must include
`Z`, `ZWARN`, `NUMOBS` and `TARGETID` and BE SORTED ON TARGETID
to match `targets` row-by-row. May also contain `NUMOBS_MORE` if
this isn't the first time through MTL and `NUMOBS > 0`.
obscon : :class:`str`
A combination of strings that are in the desitarget bitmask yaml
file (specifically in `desitarget.targetmask.obsconditions`), e.g.
"DARK". Governs the behavior of how priorities are set based
on "obsconditions" in the desitarget bitmask yaml file.
Returns
-------
:class:`~numpy.array`
Integer array of number of additional observations (NUMOBS_MORE).
Notes
-----
- Will automatically detect if the passed targets are main
survey, commissioning or SV and behave accordingly.
- Most targets are updated to NUMOBS_MORE = NUMOBS_INIT-NUMOBS.
Special case for the main survey is QSOs below the midz, which
get 1 extra observation.
"""
# ADM check input arrays are sorted to match row-by-row on TARGETID.
assert np.all(targets["TARGETID"] == zcat["TARGETID"])
# ADM determine whether the input targets are main survey, cmx or SV.
colnames, masks, survey = main_cmx_or_sv(targets, scnd=True)
# ADM the target bits/names should be shared between main survey and SV.
if survey != 'cmx':
desi_target, bgs_target, mws_target, scnd_target = colnames
desi_mask, bgs_mask, mws_mask, scnd_mask = masks
else:
cmx_mask = masks[0]
# ADM main case, just decrement by NUMOBS.
numobs_more = np.maximum(0, targets['NUMOBS_INIT'] - zcat['NUMOBS'])
# ADM apply special QSO behavior, but only in dark time and after
# ADM some observations have occurred.
if survey == 'main' and np.any(zcat["NUMOBS"] > 0):
if (obsconditions.mask(obscon) & obsconditions.mask("DARK")) != 0:
# ADM A QSO target that is confirmed to have a redshift at
# ADM z < midzcut will need to drop by 2 total observations
# ADM (midzcut is defined at the top of this module).
isqso = targets[desi_target] & desi_mask.QSO != 0
# ADM "lock in" the numobs state for existing Ly-Alpha QSOs.
lya = targets["PRIORITY"] == desi_mask["QSO"].priorities["MORE_ZGOOD"]
# ADM the mocks may not include the secondary targets.
if scnd_target in targets.dtype.names:
for scxname in scnd_mask.names():
if scnd_mask[scxname].flavor == "QSO":
isqso |= targets[scnd_target] & scnd_mask[scxname] != 0
# ADM the definition used for "not-LyA" in calc_priority.
midz = (zcat['Z'] < zcut) & ((zcat['Z_QN'] < zcut)
| (zcat["IS_QSO_QN"] != 1))
# ADM the likely low-z sources get fewer (2) observations.
loz = ((zcat['Z'] < midzcut) | (zcat['Z_QN'] < midzcut) |
(zcat["IS_QSO_QN"] != 1))
ii = isqso & midz & loz & ~lya
numobs_more[ii] = np.maximum(0, numobs_more[ii] - 2)
return numobs_more
[docs]def calc_priority(targets, zcat, obscon, state=False):
"""
Calculate target priorities from masks, observation/redshift status.
Parameters
----------
targets : :class:`~numpy.ndarray`
numpy structured array or astropy Table of targets. Must include
the columns `DESI_TARGET`, `BGS_TARGET`, `MWS_TARGET` (or their
SV/cmx equivalents) and `TARGETID`. For Main Survey targets, must
also contain `PRIORITY` if this isn't the first time through MTL,
which is used to "lock in" the state of Lyman-Alpha quasars.
zcat : :class:`~numpy.ndarray`
numpy structured array or Table of redshift info. Must include
`Z`, `ZWARN`, `NUMOBS` and `TARGETID` and BE SORTED ON TARGETID
to match `targets` row-by-row. May also contain `NUMOBS_MORE` if
this isn't the first time through MTL and `NUMOBS > 0`.
obscon : :class:`str`
A combination of strings that are in the desitarget bitmask yaml
file (specifically in `desitarget.targetmask.obsconditions`), e.g.
"DARK|GRAY". Governs the behavior of how priorities are set based
on "obsconditions" in the desitarget bitmask yaml file.
state : :class:`bool`
If ``True`` then also return a string denoting the state that
was set. The state is a string combining the observational
state (e.g. "DONE", "MORE_ZGOOD") from the targeting yaml file
and the target type (e.g. "ELG", "LRG").
Returns
-------
:class:`~numpy.array`
integer array of priorities.
:class:`~numpy.array`
string array of states. Only returned if `state`=``True``
Notes
-----
- If a target passes multiple selections, highest priority wins.
- Will automatically detect if the passed targets are main
survey, commissioning or SV and behave accordingly.
"""
# ADM check input arrays are sorted to match row-by-row on TARGETID.
assert np.all(targets["TARGETID"] == zcat["TARGETID"])
# ADM determine whether the input targets are main survey, cmx or SV.
colnames, masks, survey = main_cmx_or_sv(targets, scnd=True)
# ADM the target bits/names should be shared between main survey and SV.
if survey != 'cmx':
desi_target, bgs_target, mws_target, scnd_target = colnames
desi_mask, bgs_mask, mws_mask, scnd_mask = masks
else:
cmx_mask = masks[0]
# Default is 0 priority, i.e. do not observe.
priority = np.zeros(len(targets), dtype='i8')
# ADM set up a string to record the state of each target.
from desitarget.mtl import mtldatamodel
target_state = np.zeros(len(targets),
dtype=mtldatamodel["TARGET_STATE"].dtype)
# Determine which targets have been observed.
unobs = (zcat["NUMOBS"] == 0)
log.debug('calc_priority has %d unobserved targets' % (np.sum(unobs)))
if np.all(unobs):
done = np.zeros(len(targets), dtype=bool)
zgood = np.zeros(len(targets), dtype=bool)
zwarn = np.zeros(len(targets), dtype=bool)
lya = np.zeros(len(targets), dtype=bool)
else:
nmore = zcat["NUMOBS_MORE"]
assert np.all(nmore >= 0)
done = ~unobs & (nmore == 0)
zgood = ~unobs & (nmore > 0) & (zcat['ZWARN'] == 0)
zwarn = ~unobs & (nmore > 0) & (zcat['ZWARN'] != 0)
if survey == 'main':
# ADM used to "lock in" the state of LyA QSOs...
lya = targets["PRIORITY"] == desi_mask["QSO"].priorities["MORE_ZGOOD"]
# ADM ...once they're observed...
lya &= ~unobs
# ADM ... and until they're done.
lya &= ~done
# zgood, zwarn, done, and unobs should be mutually exclusive and cover all
# targets.
assert not np.any(unobs & zgood)
assert not np.any(unobs & zwarn)
assert not np.any(unobs & done)
assert not np.any(zgood & zwarn)
assert not np.any(zgood & done)
assert not np.any(zwarn & done)
assert np.all(unobs | done | zgood | zwarn)
# DESI dark time targets.
if survey != 'cmx':
if desi_target in targets.dtype.names:
# ADM set initial state of CALIB for potential calibration targets.
names = ('SKY', 'BAD_SKY', 'SUPP_SKY',
'STD_FAINT', 'STD_WD', 'STD_BRIGHT')
for name in names:
# ADM only update states for passed observing conditions.
pricon = obsconditions.mask(desi_mask[name].obsconditions)
if (obsconditions.mask(obscon) & pricon) != 0:
ii = (targets[desi_target] & desi_mask[name]) != 0
target_state[ii] = "CALIB"
names = ('ELG_VLO', 'ELG_LOP', 'ELG_HIP', 'LRG')
# ADM for sv3 the ELG guiding columns were ELG and ELG_HIP.
if survey == 'sv3':
names = ('ELG_LOP', 'ELG_HIP', 'LRG')
for name in names:
# ADM only update priorities for passed observing conditions.
pricon = obsconditions.mask(desi_mask[name].obsconditions)
if (obsconditions.mask(obscon) & pricon) != 0:
ii = (targets[desi_target] & desi_mask[name]) != 0
for sbool, sname in zip(
[unobs, done, zgood, zwarn],
["UNOBS", "DONE", "MORE_ZGOOD", "MORE_ZWARN"]
):
# ADM update priorities and target states.
Mxp = desi_mask[name].priorities[sname]
# ADM tiered system in SV3. Decrement MORE_ZWARN
# ADM priority using the bit's zwarndecrement.
if survey == "sv3" and sname == "MORE_ZWARN":
zwd = desi_mask[name].priorities["ZWARN_DECREMENT"]
Mxp -= zwd * zcat[ii & sbool]["NUMOBS"]
# ADM update states BEFORE changing priorities.
ts = "{}|{}".format(name, sname)
target_state[ii & sbool] = np.where(
priority[ii & sbool] < Mxp, ts, target_state[ii & sbool])
priority[ii & sbool] = np.where(
priority[ii & sbool] < Mxp, Mxp, priority[ii & sbool])
# QSO could be Lyman-alpha or Tracer.
name = 'QSO'
# ADM only update priorities for passed observing conditions.
pricon = obsconditions.mask(desi_mask[name].obsconditions)
if (obsconditions.mask(obscon) & pricon) != 0:
ii = (targets[desi_target] & desi_mask[name]) != 0
# ADM LyA QSOs require more observations.
# ADM (zcut is defined at the top of this module).
# ADM Main Survey decisions are made using QN/Redrock.
if survey == "main":
good_hiz = (zcat['Z'] >= zcut) | ((zcat['Z_QN'] >= zcut) &
(zcat["IS_QSO_QN"] == 1))
# ADM all non-LyA-QSOs need more low-priority passes
# ADM in the Main Survey. The mid-z QSOs get 4 passes
# ADM at this lower priority, as requested by some
# ADM secondaries, which is set in calc_numobs_more.
good_midz = (zcat['Z'] < zcut) & ((zcat['Z_QN'] < zcut) |
(zcat["IS_QSO_QN"] != 1))
# ADM good_hiz & good_midz should never occur in
# ADM the Main Survey as they're complements.
assert not np.any(good_hiz & good_midz)
# ADM flip to the done state if we've reached it.
good_hiz &= ~done
good_midz &= ~done
# ADM Main Survey QSOs have no zwarn priority state
# ADM but do have a Lyman-alpha (lya) "locked-in" state.
sbools = [unobs, done, good_hiz, good_midz,
~good_hiz & ~good_midz, lya]
snames = ["UNOBS", "DONE", "MORE_ZGOOD", "MORE_MIDZQSO",
"DONE", "MORE_ZGOOD"]
# ADM In SV decisions were made without QN.
elif survey == "sv3":
good_hiz = zgood & (zcat['Z'] >= zcut)
# ADM SV3 specified mid-z QSOs required more passes.
good_midz = zgood & (zcat['Z'] >= midzcut) & (zcat['Z'] < zcut)
# ADM in SV3 we had a zwarn priority state for QSOs.
sbools = [unobs, done, good_hiz, good_midz,
~good_hiz & ~good_midz, zwarn]
snames = ["UNOBS", "DONE", "MORE_ZGOOD", "MORE_MIDZQSO",
"DONE", "MORE_ZWARN"]
else:
good_hiz = zgood & (zcat['Z'] >= zcut)
good_midz = zgood & (zcat['Z'] < zcut)
sbools = [unobs, done, good_hiz, good_midz,
~good_hiz & ~good_midz, zwarn]
snames = ["UNOBS", "DONE", "MORE_ZGOOD", "MORE_MIDZQSO",
"DONE", "MORE_ZWARN"]
for sbool, sname in zip(sbools, snames):
# ADM update priorities and target states.
Mxp = desi_mask[name].priorities[sname]
# ADM tiered system in SV3. Decrement MORE_ZWARN
# ADM priority using the bit's zwarndecrement.
if survey == "sv3" and sname == "MORE_ZWARN":
zwd = desi_mask[name].priorities["ZWARN_DECREMENT"]
Mxp -= zwd * zcat[ii & sbool]["NUMOBS"]
# ADM update states BEFORE changing priorities.
ts = "{}|{}".format(name, sname)
target_state[ii & sbool] = np.where(
priority[ii & sbool] < Mxp, ts, target_state[ii & sbool])
priority[ii & sbool] = np.where(
priority[ii & sbool] < Mxp, Mxp, priority[ii & sbool])
# BGS targets.
if bgs_target in targets.dtype.names:
for name in bgs_mask.names():
# ADM only update priorities for passed observing conditions.
pricon = obsconditions.mask(bgs_mask[name].obsconditions)
if (obsconditions.mask(obscon) & pricon) != 0:
ii = (targets[bgs_target] & bgs_mask[name]) != 0
for sbool, sname in zip(
[unobs, done, zgood, zwarn],
["UNOBS", "DONE", "MORE_ZGOOD", "MORE_ZWARN"]
):
# ADM update priorities and target states.
Mxp = bgs_mask[name].priorities[sname]
# ADM tiered system in SV3. Decrement MORE_ZWARN
# ADM priority using the bit's zwarndecrement.
if survey == "sv3" and sname == "MORE_ZWARN":
zwd = bgs_mask[name].priorities["ZWARN_DECREMENT"]
Mxp -= zwd * zcat[ii & sbool]["NUMOBS"]
# ADM update states BEFORE changing priorities.
ts = "{}|{}".format(name, sname)
target_state[ii & sbool] = np.where(
priority[ii & sbool] < Mxp, ts, target_state[ii & sbool])
priority[ii & sbool] = np.where(
priority[ii & sbool] < Mxp, Mxp, priority[ii & sbool])
# MWS targets.
if mws_target in targets.dtype.names:
# ADM set initial state of CALIB for potential calibration targets.
stdnames = ('GAIA_STD_FAINT', 'GAIA_STD_WD', 'GAIA_STD_BRIGHT')
for name in mws_mask.names():
# ADM only update priorities for passed observing conditions.
pricon = obsconditions.mask(mws_mask[name].obsconditions)
if (obsconditions.mask(obscon) & pricon) != 0:
ii = (targets[mws_target] & mws_mask[name]) != 0
# ADM standards have no priority.
if name in stdnames:
target_state[ii] = "CALIB"
else:
for sbool, sname in zip(
[unobs, done, zgood, zwarn],
["UNOBS", "DONE", "MORE_ZGOOD", "MORE_ZWARN"]
):
# ADM update priorities and target states.
Mxp = mws_mask[name].priorities[sname]
# ADM tiered system in SV3. Decrement MORE_ZWARN
# ADM priority using the bit's zwarndecrement.
if survey == "sv3" and sname == "MORE_ZWARN":
zwd = mws_mask[name].priorities["ZWARN_DECREMENT"]
Mxp -= zwd * zcat[ii & sbool]["NUMOBS"]
# ADM update states BEFORE changing priorities.
ts = "{}|{}".format(name, sname)
target_state[ii & sbool] = np.where(
priority[ii & sbool] < Mxp, ts, target_state[ii & sbool])
priority[ii & sbool] = np.where(
priority[ii & sbool] < Mxp, Mxp, priority[ii & sbool])
# ADM Secondary targets.
if scnd_target in targets.dtype.names:
# APC Secondaries only drive updates for specific DESI_TARGET
# APC bits (https://github.com/desihub/desitarget/pull/530).
# APC Default behaviour is that targets with SCND_ANY bits set will
# APC ONLY be updated based on their secondary targetmask parameters IF
# APC they have NO primary target bits set (hence == on next line).
scnd_update = targets[desi_target] == desi_mask['SCND_ANY']
log.info('{} scnd targets to be updated as secondary-only'.format(
scnd_update.sum()))
# APC The exception to the rule above is that a subset of bits flagged
# APC with updatemws=True in the targetmask can drive updates for a
# APC subset of primary bits corresponding to MWS targets and
# APC standards. We first create a bitmask of those permitted seconday
# APC bits.
permit_scnd_bits = 0
for name in scnd_mask.names():
if survey == 'main':
# updatemws only defined for main survey targetmask.
if scnd_mask[name].updatemws:
permit_scnd_bits |= scnd_mask[name]
else:
# Before updatemws was introduced, all scnd bits
# were permitted to update MWS targets.
permit_scnd_bits |= scnd_mask[name]
# APC Now we flag any target combining the permitted secondary bits
# APC and the restricted set of primary bits.
permit_scnd = (targets[scnd_target] & permit_scnd_bits) != 0
# APC Allow changes to primaries to be driven by the status of
# APC their matched secondary bits if the DESI_TARGET bitmask has any
# APC of the following bits set, but not any other bits.
update_from_scnd_bits = (
desi_mask['SCND_ANY'] | desi_mask['MWS_ANY'] |
desi_mask['STD_BRIGHT'] | desi_mask['STD_FAINT'] |
desi_mask['STD_WD'])
permit_scnd &= ((targets[desi_target] & ~update_from_scnd_bits) == 0)
log.info('{} more scnd targets allowed to update MWS primaries'.format(
(permit_scnd & ~scnd_update).sum()))
# APC Updateable targets are either pure secondary or explicitly permitted
scnd_update |= permit_scnd
log.info('{} scnd targets to be updated in total'.format(
scnd_update.sum()))
if np.any(scnd_update):
for name in scnd_mask.names():
# ADM only update priorities for passed observing conditions.
pricon = obsconditions.mask(scnd_mask[name].obsconditions)
if (obsconditions.mask(obscon) & pricon) != 0:
ii = (targets[scnd_target] & scnd_mask[name]) != 0
ii &= scnd_update
# ADM scnd LyA QSOs may require more observations.
# ADM (zcut is defined at the top of this module).
# ADM Main Survey decisions are made using QN/Redrock.
if survey == "main":
good_hiz = (zcat['Z'] >= zcut) | ((zcat['Z_QN'] >= zcut) &
(zcat["IS_QSO_QN"] == 1))
# ADM all non-LyA-QSOs need more low-priority passes
# ADM in the Main Survey. The mid-z QSOs get 4 passes
# ADM at this lower priority, as requested by some
# ADM secondaries, which is set in calc_numobs_more.
good_midz = (zcat['Z'] < zcut) & ((zcat['Z_QN'] < zcut) |
(zcat["IS_QSO_QN"] != 1))
# ADM good_hiz & good_midz should never occur in
# ADM the Main Survey as they're complements.
assert not np.any(good_hiz & good_midz)
# ADM flip to the done state if we've reached it.
good_hiz &= ~done
good_midz &= ~done
# ADM In SV decisions were made without QN.
elif survey == "sv3":
good_hiz = zgood & (zcat['Z'] >= zcut)
# ADM SV3 specified mid-z QSOs required more passes.
good_midz = zgood & (zcat['Z'] >= midzcut) & (zcat['Z'] < zcut)
else:
good_hiz = zgood & (zcat['Z'] >= zcut)
good_midz = zgood & (zcat['Z'] < zcut)
# ADM secondary QSOs need processed like primary QSOs.
if scnd_mask[name].flavor == "QSO":
if survey == "main":
# ADM Main Survey QSOs have no zwarn priority state
# ADM but do have a Lyman-alpha (lya) "locked-in" state.
sbools = [unobs, done, good_hiz, good_midz,
~good_hiz & ~good_midz, lya]
snames = ["UNOBS", "DONE", "MORE_ZGOOD", "MORE_MIDZQSO",
"DONE", "MORE_ZGOOD"]
else:
sbools = [unobs, done, good_hiz, good_midz,
~good_hiz & ~good_midz, zwarn]
snames = ["UNOBS", "DONE", "MORE_ZGOOD", "MORE_MIDZQSO",
"DONE", "MORE_ZWARN"]
else:
sbools = [unobs, done, zgood, zwarn]
snames = ["UNOBS", "DONE", "MORE_ZGOOD", "MORE_ZWARN"]
for sbool, sname in zip(sbools, snames):
# ADM update priorities and target states.
Mxp = scnd_mask[name].priorities[sname]
# ADM tiered system in SV3. Decrement MORE_ZWARN
# ADM priority using the bit's zwarndecrement.
# if survey == "sv3" and sname == "MORE_ZWARN":
# zwd = scnd_mask[name].priorities["ZWARN_DECREMENT"]
# Mxp -= zwd * zcat[ii & sbool]["NUMOBS"]
# ADM update states BEFORE changing priorities.
ts = "{}|{}".format(name, sname)
target_state[ii & sbool] = np.where(
priority[ii & sbool] < Mxp, ts, target_state[ii & sbool])
priority[ii & sbool] = np.where(
priority[ii & sbool] < Mxp, Mxp, priority[ii & sbool])
# Special case: IN_BRIGHT_OBJECT means priority=-1 no matter what.
ii = (targets[desi_target] & desi_mask.IN_BRIGHT_OBJECT) != 0
priority[ii] = -1
target_state[ii] = "IN_BRIGHT_OBJECT"
# ADM Special case: SV-like commissioning targets.
if 'CMX_TARGET' in targets.dtype.names:
priority = _cmx_calc_priority(targets, priority, obscon,
unobs, done, zgood, zwarn, cmx_mask, obsconditions)
if state:
return priority, target_state
return priority
[docs]def _cmx_calc_priority(targets, priority, obscon,
unobs, done, zgood, zwarn, cmx_mask, obsconditions):
"""Special-case logic for target priorities in CMX.
Parameters
----------
targets : :class:`~numpy.ndarray`
numpy structured array or astropy Table of targets. Must include
the column `CMX_TARGET`.
priority : :class:`~numpy.ndarray`
Initial priority values set, in calc_priorities().
obscon : :class:`str`
A combination of strings that are in the desitarget bitmask yaml
file (specifically in `desitarget.targetmask.obsconditions`), e.g.
"DARK|GRAY". Governs the behavior of how priorities are set based
on "obsconditions" in the desitarget bitmask yaml file.
unobs : :class:`~numpy.ndarray`
Boolean flag on targets indicating state UNOBS.
done : :class:`~numpy.ndarray`
Boolean flag on targets indicating state DONE.
zgood : :class:`~numpy.ndarray`
Boolean flag on targets indicating state ZGOOD.
zwarn : :class:`~numpy.ndarray`
Boolean flag on targets indicating state ZWARN.
cmx_mask : :class:`~desiutil.bitmask.BitMask`
The CMX target bitmask.
obscondtions : :class:`~desiutil.bitmask.BitMask`
The CMX obsconditions bitmask.
Returns
-------
:class:`~numpy.ndarray`
The updated priority values.
Notes
-----
- Intended to be called only from within calc_priority(), where any
pre-processing of the target state flags (uobs, done, zgood, zwarn) is
handled.
"""
# Build a permitted list of targets to update
names_to_update = ['SV0_' + label for label in ('STD_FAINT', 'STD_BRIGHT',
'BGS', 'MWS', 'WD', 'MWS_FAINT',
'MWS_CLUSTER', 'MWS_CLUSTER_VERYBRIGHT')]
names_to_update.extend(['BACKUP_BRIGHT', 'BACKUP_FAINT'])
for name in names_to_update:
pricon = obsconditions.mask(cmx_mask[name].obsconditions)
if (obsconditions.mask(obscon) & pricon) != 0:
ii = (targets['CMX_TARGET'] & cmx_mask[name]) != 0
priority[ii & unobs] = np.maximum(priority[ii & unobs], cmx_mask[name].priorities['UNOBS'])
priority[ii & done] = np.maximum(priority[ii & done], cmx_mask[name].priorities['DONE'])
priority[ii & zgood] = np.maximum(priority[ii & zgood], cmx_mask[name].priorities['MORE_ZGOOD'])
priority[ii & zwarn] = np.maximum(priority[ii & zwarn], cmx_mask[name].priorities['MORE_ZWARN'])
return priority
[docs]def resolve(targets):
"""Resolve which targets are primary in imaging overlap regions.
Parameters
----------
targets : :class:`~numpy.ndarray`
Rec array of targets. Must have columns "RA" and "DEC" and
either "RELEASE" or "PHOTSYS" or "TARGETID".
Returns
-------
:class:`~numpy.ndarray`
The original target list trimmed to only objects from the "northern"
photometry in the northern imaging area and objects from "southern"
photometry in the southern imaging area.
"""
# ADM retrieve the photometric system from the RELEASE.
from desitarget.io import release_to_photsys, desitarget_resolve_dec
if 'PHOTSYS' in targets.dtype.names:
photsys = targets["PHOTSYS"]
else:
if 'RELEASE' in targets.dtype.names:
photsys = release_to_photsys(targets["RELEASE"])
else:
_, _, release, _, _, _ = decode_targetid(targets["TARGETID"])
photsys = release_to_photsys(release)
# ADM a flag of which targets are from the 'N' photometry.
from desitarget.cuts import _isonnorthphotsys
photn = _isonnorthphotsys(photsys)
# ADM grab the declination used to resolve targets.
split = desitarget_resolve_dec()
# ADM determine which targets are north of the Galactic plane. As
# ADM a speed-up, bin in ~1 sq.deg. HEALPixels and determine
# ADM which of those pixels are north of the Galactic plane.
# ADM We should never be as close as ~1o to the plane.
from desitarget.geomask import is_in_gal_box, pixarea2nside
nside = pixarea2nside(1)
theta, phi = np.radians(90-targets["DEC"]), np.radians(targets["RA"])
pixnum = hp.ang2pix(nside, theta, phi, nest=True)
# ADM find the pixels north of the Galactic plane...
allpix = np.arange(hp.nside2npix(nside))
theta, phi = hp.pix2ang(nside, allpix, nest=True)
ra, dec = np.degrees(phi), 90-np.degrees(theta)
pixn = is_in_gal_box([ra, dec], [0., 360., 0., 90.], radec=True)
# ADM which targets are in pixels north of the Galactic plane.
galn = pixn[pixnum]
# ADM which targets are in the northern imaging area.
arean = (targets["DEC"] >= split) & galn
# ADM retain 'N' targets in 'N' area and 'S' in 'S' area.
keep = (photn & arean) | (~photn & ~arean)
return targets[keep]
[docs]def finalize(targets, desi_target, bgs_target, mws_target,
sky=False, randoms=False, survey='main', darkbright=False,
gaiadr=None, gdr=None, targetid=None, forcerelease=False):
"""Return new targets array with added/renamed columns
Parameters
----------
targets : :class:`~numpy.ndarray`
numpy structured array of targets.
desi_target : :class:`~numpy.ndarray`
1D array of target selection bit flags.
bgs_target : :class:`~numpy.ndarray`
1D array of target selection bit flags.
mws_target : :class:`~numpy.ndarray`
1D array of target selection bit flags.
sky : :class:`bool`, defaults to ``False``
Pass ``True`` for sky targets, ``False`` otherwise.
randoms : :class:`bool`, defaults to ``False``
``True`` if `targets` is a random catalog, ``False`` otherwise.
survey : :class:`str`, defaults to `main`
Specifies which target masks yaml file to use. Options are `main`,
`cmx` and `svX` (where X = 1, 2, 3 etc.) for the main survey,
commissioning and an iteration of SV.
darkbright : :class:`bool`, optional, defaults to ``False``
If sent, then split `NUMOBS_INIT` and `PRIORITY_INIT` into
`NUMOBS_INIT_DARK`, `NUMOBS_INIT_BRIGHT`, `PRIORITY_INIT_DARK`
and `PRIORITY_INIT_BRIGHT` and calculate values appropriate
to "BRIGHT" and "DARK|GRAY" observing conditions.
gaiadr : :class:`int`, optional, defaults to ``None``
If passed and not ``None``, then build the `TARGETID` from the
"GAIA_OBJID" and "GAIA_BRICKID" columns in the passed `targets`,
and set the `gaiadr` part of `TARGETID` to whatever is passed.
"RELEASE" is set to zero.
gdr : :class:`int`, defaults to ``None``
An alternate version of `gaiadr` where the "OBJID", "BRICKID" and
"RELEASE" columns are used as normal, but `gdr` is sent to
:func:`desitarget.targets.encode_targetid` as the gaiadr bit.
targetid : :class:`int64`, optional, defaults to ``None``
In the mocks we compute `TARGETID` outside this function.
Returns
-------
:class:`~numpy.ndarray`
new targets structured array with the following additions:
* renaming OBJID -> BRICK_OBJID (it is only unique within a brick).
* renaming TYPE -> MORPHTYPE (used downstream in other contexts).
* Adding new columns:
- TARGETID: unique ID across all bricks or Gaia files.
- DESI_TARGET: dark time survey target selection flags.
- MWS_TARGET: bright time MWS target selection flags.
- BGS_TARGET: bright time BGS target selection flags.
- PRIORITY_INIT: initial priority for observing target.
- SUBPRIORITY: a placeholder column that is set to zero.
- NUMOBS_INIT: initial number of observations for target.
- OBSCONDITIONS: bitmask of observation conditions.
Notes
-----
- SUBPRIORITY is the only column that isn't populated. This is
because it's easier to populate it in a reproducible fashion
when collecting targets rather than on a per-brick basis
when this function is called. It's set to all zeros.
- Only one of `gaiadr` and `gdr` can be input.
"""
if gaiadr is not None and gdr is not None:
msg = "only one of gaiadr and gdr can be input (and not None)"
log.critical(msg)
raise IOError(msg)
ntargets = len(targets)
assert ntargets == len(desi_target)
assert ntargets == len(bgs_target)
assert ntargets == len(mws_target)
# - OBJID in tractor files is only unique within the brick; rename and
# - create a new unique TARGETID
targets = rfn.rename_fields(targets,
{'OBJID': 'BRICK_OBJID', 'TYPE': 'MORPHTYPE'})
# allow TARGETID to be passed as an input (specifically for the mocks).
if targetid is None:
if gaiadr is not None:
targetid = encode_targetid(objid=targets['GAIA_OBJID'],
brickid=targets['GAIA_BRICKID'],
release=0,
mock=int(randoms),
sky=int(sky),
gaiadr=gaiadr)
else:
targetid = encode_targetid(objid=targets['BRICK_OBJID'],
brickid=targets['BRICKID'],
release=targets['RELEASE'],
mock=int(randoms),
sky=int(sky),
gaiadr=gdr)
assert ntargets == len(targetid)
nodata = np.zeros(ntargets, dtype='int')-1
subpriority = np.zeros(ntargets, dtype='float')
# ADM new columns are different depending on SV/cmx/main survey.
if survey == 'main':
colnames = ['DESI_TARGET', 'BGS_TARGET', 'MWS_TARGET']
elif survey == 'cmx':
colnames = ['CMX_TARGET']
elif survey[:2] == 'sv':
colnames = ["{}_{}_TARGET".format(survey.upper(), tc)
for tc in ["DESI", "BGS", "MWS"]]
else:
msg = "survey must be 'main', 'cmx' or 'svX' (X=1,2..etc.), not {}!" \
.format(survey)
log.critical(msg)
raise ValueError(msg)
# ADM the columns to write out and their values and formats.
cols = ["TARGETID"] + colnames + ['SUBPRIORITY', 'OBSCONDITIONS']
vals = [targetid] + [desi_target, bgs_target, mws_target][:len(colnames)] \
+ [subpriority, nodata]
forms = ['>i8'] + ['>i8', '>i8', '>i8'][:len(colnames)] + ['>f8', '>i8']
# ADM set the initial PRIORITY and NUMOBS.
if darkbright:
# ADM populate bright/dark if splitting by survey OBSCONDITIONS.
ender = ["_DARK", "_BRIGHT", "_BACKUP"]
obscon = ["DARK|GRAY", "BRIGHT", "BACKUP"]
else:
ender, obscon = [""], ["DARK|GRAY|BRIGHT|BACKUP|TWILIGHT12|TWILIGHT18"]
for edr, oc in zip(ender, obscon):
cols += ["{}_INIT{}".format(pn, edr) for pn in ["PRIORITY", "NUMOBS"]]
vals += [nodata, nodata]
forms += ['>i8', '>i8']
# ADM write the output array.
newdt = [dt for dt in zip(cols, forms)]
done = np.array(np.zeros(len(targets)), dtype=targets.dtype.descr+newdt)
for col in targets.dtype.names:
done[col] = targets[col]
for col, val in zip(cols, vals):
done[col] = val
# ADM add PRIORITY/NUMOBS columns.
for edr, oc in zip(ender, obscon):
pc, nc = "PRIORITY_INIT"+edr, "NUMOBS_INIT"+edr
done[pc], done[nc] = initial_priority_numobs(done, obscon=oc)
# ADM set the OBSCONDITIONS.
done["OBSCONDITIONS"] = set_obsconditions(done)
# ADM some final checks that the targets conform to expectations...
# ADM check that each target has a unique ID.
if len(done["TARGETID"]) != len(set(done["TARGETID"])):
msg = 'TARGETIDs are not unique!'
log.critical(msg)
raise AssertionError(msg)
# ADM check all LRG targets have LRG_1PASS/2PASS set.
# ADM we've moved away from LRG PASSes so deprecate this for now.
# if survey == 'main':
# lrgset = done["DESI_TARGET"] & desi_mask.LRG != 0
# pass1lrgset = done["DESI_TARGET"] & desi_mask.LRG_1PASS != 0
# pass2lrgset = done["DESI_TARGET"] & desi_mask.LRG_2PASS != 0
# if not np.all(lrgset == pass1lrgset | pass2lrgset):
# msg = 'Some LRG targets do not have 1PASS/2PASS set!'
# log.critical(msg)
# raise AssertionError(msg)
return done