Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Field rotation updates to avoid hexapod rotation limit errors #466

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions py/fiberassign/data/cutoff-dates.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
rundate:
std_wd: "2021-12-01T19:00:00+00:00" # std_wd not counted in per-petal-std after that rundate
etc_stuck: "2022-04-28T19:00:00+00:00" # etc fibers are treated as stuck fibers after this date
obsdate: "2025-03-01T19:00:00+00:00" # obsdate set as rundate + 1yr
37 changes: 33 additions & 4 deletions py/fiberassign/scripts/assign.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import argparse
import re

from ..utils import GlobalTimers, Logger
from ..utils import GlobalTimers, Logger, get_default_static_obsdate, get_obsdate

from ..hardware import load_hardware, get_default_exclusion_margins

Expand Down Expand Up @@ -95,9 +95,12 @@ def parse_assign(optlist=None):
"Default uses the current date. Format is "
"YYYY-MM-DDTHH:mm:ss+-zz:zz.")

parser.add_argument("--obsdate", type=str, required=False, default="2022-07-01",
parser.add_argument("--obsdate", type=str, required=False,
default=None,
help="Plan field rotations for this date (YEARMMDD, "
"or ISO 8601 YEAR-MM-DD with or without time).")
"or ISO 8601 YEAR-MM-DD with or without time); "
"default={} or rundate+1yr, according to rundate".
format(get_default_static_obsdate()))

parser.add_argument("--ha", type=float, required=False, default=0.,
help="Design for the given Hour Angle in degrees.")
Expand All @@ -106,6 +109,12 @@ def parse_assign(optlist=None):
help="Override obsdate and use this field rotation "
"for all tiles (degrees counter clockwise in CS5)")

parser.add_argument("--fieldrot_corr", type=bool, required=False,
default=None,
help="apply correction to computed fieldrot "
"(default: False if args.fieldrot is provided or "
"if args.rundate<rundate_cutoff, True else")

parser.add_argument("--dir", type=str, required=False, default=None,
help="Output directory.")

Expand Down Expand Up @@ -259,6 +268,10 @@ def parse_assign(optlist=None):
except ValueError:
args.excludemask = desi_mask.mask(args.excludemask.replace(",","|"))

# Set obsdate
if args.obsdate is None:
args.obsdate, _ = get_obsdate(rundate=args.rundate)

# convert YEARMMDD to YEAR-MM-DD to be ISO 8601 compatible
if re.match('\d{8}', args.obsdate):
year = args.obsdate[0:4]
Expand All @@ -267,6 +280,15 @@ def parse_assign(optlist=None):
#- Note: ISO8601 does not require time portion
args.obsdate = '{}-{}-{}'.format(year, mm, dd)

# Apply the field rotation correction?
# - True if fieldrot=None, rundate!=None and rundate>=rundate_cutoff
# - False else
if args.fieldrot_corr is None:
if args.fieldrot is None:
_, args.fieldrot_corr = get_obsdate(rundate=args.rundate)
else:
args.fieldrot_corr = False

# Set output directory
if args.dir is None:
if args.rundate is None:
Expand All @@ -278,8 +300,14 @@ def parse_assign(optlist=None):
args.margins = dict(pos=args.margin_pos,
petal=args.margin_petal,
gfa=args.margin_gfa)

# Print all args
for kwargs in args._get_kwargs():
log.info("{}:\t{}".format(kwargs[0], kwargs[1]))

return args


def run_assign_init(args, plate_radec=True):
"""Initialize assignment inputs.

Expand Down Expand Up @@ -311,7 +339,8 @@ def run_assign_init(args, plate_radec=True):
except ValueError:
pass
tiles = load_tiles(tiles_file=args.footprint, select=tileselect,
obstime=args.obsdate, obstheta=args.fieldrot, obsha=args.ha)
obstime=args.obsdate, obstheta=args.fieldrot, obsha=args.ha,
obsthetacorr=args.fieldrot_corr)

# Before doing significant calculations, check for pre-existing files
if not args.overwrite:
Expand Down
21 changes: 19 additions & 2 deletions py/fiberassign/tiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@

import astropy.time

from fiberassign.utils import get_default_static_obsdate, get_obstheta_corr

from ._internal import Tiles


def load_tiles(tiles_file=None, select=None, obstime=None, obstheta=None,
obsha=None):
obsha=None, obsthetacorr=False):
"""Load tiles from a file.

Load tile data either from the specified file or from the default provided
Expand All @@ -40,10 +42,16 @@ def load_tiles(tiles_file=None, select=None, obstime=None, obstheta=None,
obstheta (float): The angle in degrees to override the field rotation
of all tiles.
obsha (float): The Hour Angle in degrees to design the observation of all tiles.
obsthetacorr (optional, defaults to False): apply a correction on obstheta,
based on DocDB-8931 (bool)

Returns:
(Tiles): A Tiles object.

Notes:
2025, Feb.: added the obsthetacorr optional argument; this correction
implemented to avoid the hexapod rotation limit error, is
presented/described in DocDB-8931.
"""
# Read in the tile information
if tiles_file is None:
Expand Down Expand Up @@ -71,7 +79,7 @@ def load_tiles(tiles_file=None, select=None, obstime=None, obstheta=None,
obsdatestr = [x.isot for x in obsdate]
else:
# default to middle of the survey
obsdate = astropy.time.Time('2022-07-01')
obsdate = astropy.time.Time(get_default_static_obsdate())
obsmjd = [obsdate.mjd,] * len(keeprows)
obsdatestr = [obsdate.isot, ] * len(keeprows)

Expand All @@ -97,6 +105,15 @@ def load_tiles(tiles_file=None, select=None, obstime=None, obstheta=None,
if obsha is not None:
ha_obs[:] = obsha

# AR apply correction on the obstheta?
# AR sign is "minus" here;
# AR see get_obstheta_corr() (and p.5 of DocDB-8931)
if obsthetacorr:
theta_obs -= get_obstheta_corr(
tiles_data["DEC"][keeprows],
ha_obs,
)

tls = Tiles(tiles_data["TILEID"][keeprows], tiles_data["RA"][keeprows],
tiles_data["DEC"][keeprows],
tiles_data["OBSCONDITIONS"][keeprows],
Expand Down
103 changes: 103 additions & 0 deletions py/fiberassign/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,59 @@ def get_date_cutoff(datetype, cutoff_case):
date_cutoff = config[datetype][cutoff_case]
return date_cutoff


def get_default_static_obsdate():
"""
Returns the 'historical' default obsdate value.

Args:
None

Returns:
"2022-07-01"
"""
return "2022-07-01"


def get_obsdate(rundate=None):
"""
Returns the default obsdate: "2022-07-01" if rundate=None or rundate<obsdate_cutoff.

Args:
rundate (optional, defaults to None): rundate, in the "YYYY-MM-DDThh:mm:ss+00:00" format (string)

Returns:
obsdate: "YYYY-MM-DD" format (string)
is_after_cutoff: is rundate after rundate_cutoff? (bool)
"""
obsdate = get_default_static_obsdate()
is_after_cutoff = False
if rundate is None:
log.info(
"rundate={} -> (obsdate, is_after_cutoff)=({}, {})".format(
rundate, obsdate, is_after_cutoff
)
)
else:
assert_isoformat_utc(rundate)
rundate_mjd = Time(datetime.strptime(rundate, "%Y-%m-%dT%H:%M:%S%z")).mjd
rundate_cutoff = get_date_cutoff("rundate", "obsdate")
rundate_mjd_cutoff = Time(datetime.strptime(rundate_cutoff, "%Y-%m-%dT%H:%M:%S%z")).mjd
if rundate_mjd >= rundate_mjd_cutoff:
is_after_cutoff = True
yyyy = int(rundate[:4])
obsdate = "{}{}".format(yyyy + 1, rundate[4:10])
log.info(
"rundate={} >= rundate_cutoff={} -> (obsdate, is_after_cutoff)=({}, {})".format(
rundate, rundate_cutoff, obsdate, is_after_cutoff)
)
else:
log.info(
"rundate={} < rundate_cutoff={} -> (obsdate, is_after_cutoff)=({}, {})".format(
rundate, rundate_cutoff, obsdate, is_after_cutoff)
)
return obsdate, is_after_cutoff


def get_svn_version(svn_dir):
"""
Expand Down Expand Up @@ -327,3 +380,53 @@ def get_rev_fiberassign_changes(svndir, rev, subdirs=None):
d["REVISION"] = np.array([rev for fn in fns], dtype=int)
d["CHANGE"] = np.array(changes, dtype=str)
return d


def get_obstheta_corr(decs, has, clip_arcsec=600.):
"""
Returns the computed correction to be applied to the field rotation
computed in fiberassign.tiles.load_tiles().
The correction should be applied as: obsthetas[deg] -= obstheta_corrs[deg].

Args:
decs: tile declinations (float or np array of floats)
has: hour angles (float or np array of floats)
clip_arcsec (optional, defaults to 600): abs(obstheta_corrs) is
forced to be <obstheta_corrs (float)

Returns:
obstheta_corrs: correction (in deg) to be applied (float or np array of floats)

Notes:
See DocDB-8931 for details.
During observations, PlateMaker computes the required field rotation,
then asks the hexapod to be rotated by
ROTOFFST = FIELDROT - PM_REQ_FIELDROT,
where FIELDROT is the field rotation coming from fiberassign.tiles.load_tiles().
When abs(ROTOFFST)>600 arcsec, the move is denied, and the exposure aborted.
Correction computed here is a fit to ROTOFFST=f(DEC), plus a fit on the residuals.
"""
assert clip_arcsec >= 0
isoneval = isinstance(decs, float)
if isoneval:
decs, has = np.atleast_1d(decs), np.atleast_1d(has)
# AR fitted function, ROTOFFST[arcsec] = f(DEC)
# AR rescale decs into [0, 1]
xs = (90. - decs) / 180.
rotoffsts = 937.60578 - 697.06513 * xs ** -0.18835
# AR fitted function to the residuals, residuals[arcsec] = f(HA)
xs = (90. + has) / 180.
residuals = -113.90162 + 222.18009 * xs
sel = xs > 0.5
residuals[sel] = -245.49007 + 485.35700 * xs[sel]
# AR total correction
obstheta_corrs = rotoffsts + residuals
# AR clip
obstheta_corrs = np.clip(obstheta_corrs, -clip_arcsec, clip_arcsec)
# AR switch to degrees
obstheta_corrs /= 3600
if isoneval:
decs, has = decs[0], has[0]
return obstheta_corrs[0]
else:
return obstheta_corrs