diff --git a/py/fiberassign/data/cutoff-dates.yaml b/py/fiberassign/data/cutoff-dates.yaml index 66dc97bd..341ed89c 100644 --- a/py/fiberassign/data/cutoff-dates.yaml +++ b/py/fiberassign/data/cutoff-dates.yaml @@ -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 diff --git a/py/fiberassign/scripts/assign.py b/py/fiberassign/scripts/assign.py index 5cef659c..a6e30bd0 100644 --- a/py/fiberassign/scripts/assign.py +++ b/py/fiberassign/scripts/assign.py @@ -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 @@ -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.") @@ -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 + # - 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: @@ -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. @@ -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: diff --git a/py/fiberassign/tiles.py b/py/fiberassign/tiles.py index 5d396bea..98f0fb40 100644 --- a/py/fiberassign/tiles.py +++ b/py/fiberassign/tiles.py @@ -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 @@ -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: @@ -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) @@ -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], diff --git a/py/fiberassign/utils.py b/py/fiberassign/utils.py index 15003fb7..8365f882 100644 --- a/py/fiberassign/utils.py +++ b/py/fiberassign/utils.py @@ -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, 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): """ @@ -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 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