diff --git a/src/schedlib/instrument.py b/src/schedlib/instrument.py index c24a16e..3e8a81e 100644 --- a/src/schedlib/instrument.py +++ b/src/schedlib/instrument.py @@ -13,19 +13,29 @@ @dataclass(frozen=True) class CalTarget: source: str - array_query: str + array_query: Union(str, list) el_bore: float tag: str t0: dt.datetime = None t1: dt.datetime = None - boresight_rot: float = 0 - allow_partial: bool = False + ra: float = None + dec: float = None + boresight_rot: float = None + allow_partial: Union(bool, list) = False drift: bool = True az_branch: Optional[float] = None az_speed: Optional[float]= None az_accel: Optional[float] = None source_direction: Optional[str] = None - from_table: Optional[bool] = None + from_table: bool = False + +@dataclass(frozen=True) +class WiregridTarget: + name: str + t0: dt.datetime + t1: dt.datetime + tag: str + boresight_rot: float = None @dataclass(frozen=True) class ScanBlock(core.NamedBlock): @@ -333,14 +343,8 @@ def parse_sequence_from_toast_sat(ifile): List of ScanBlock objects parsed from the input file. """ - - #columns = ["start_utc", "stop_utc", "rotation", "patch", "az_min", "az_max", "el", "pass", "sub"] - #columns = ["start_utc", "stop_utc", "rotation", "az_min", "az_max", "el", "pass", "sub", "patch"] - #columns = ["start_utc", "stop_utc", "hwp_dir", "rotation", "az_min", "az_max", "el", "pass", "sub", "patch"] - # columns = ["start_utc", "stop_utc", "hwp_dir", "rotation", "az_min", "az_max", - # "el", "speed", "accel", "#", "pass", "sub", "uid", "patch"] columns = ["start_utc", "stop_utc", "hwp_dir", "rotation", - "az_min", "az_max", "el", "speed", "accel", "#", "pass", + "az_min", "az_max", "el", "speed", "accel", "priority", "type", "pass", "sub", "uid", "patch" ] @@ -354,22 +358,51 @@ def parse_sequence_from_toast_sat(ifile): df = pd.read_csv(ifile, skiprows=i, delimiter="|", names=columns, comment='#') blocks = [] for _, row in df.iterrows(): - block = ScanBlock( - name=_escape_string(row['patch'].strip()), + if row['type'] != "None": + block = ScanBlock( + name=_escape_string(row['patch'].strip()), + t0=u.str2datetime(row['start_utc']), + t1=u.str2datetime(row['stop_utc']), + alt=row['el'], + az=row['az_min'], + az_speed=row['speed'], + az_accel=row['accel'], + throw=np.abs(row['az_max'] - row['az_min']), + boresight_angle=row['rotation'], + priority=row['priority'], + tag=_escape_string(row['uid'].strip()), + hwp_dir=(row['hwp_dir'] == 1) if 'hwp_dir' in row else None + ) + blocks.append(block) + return blocks + +def parse_wiregrid_targets_from_file(ifile): + columns = ["start_utc", "stop_utc", "type", "uid", + "remark" + ] + # count the number of lines to skip + with open(ifile) as f: + for i, l in enumerate(f): + if l.startswith('#'): + continue + else: + break + df = pd.read_csv(ifile, skiprows=i, delimiter="|", names=columns, comment='#') + wiregrid_targets = [] + + for _, row in df.iterrows(): + name = _escape_string(row['remark'].strip()).lower() + wiregrid_target = WiregridTarget( + name='wiregrid_gain' if 'gain' in name else 'wiregrid_time_const', t0=u.str2datetime(row['start_utc']), t1=u.str2datetime(row['stop_utc']), - alt=row['el'], - az=row['az_min'], - az_speed=row['speed'], - az_accel=row['accel'], - throw=np.abs(row['az_max'] - row['az_min']), - boresight_angle=row['rotation'], - priority=row['#'], tag=_escape_string(row['uid'].strip()), - hwp_dir=(row['hwp_dir'] == 1) if 'hwp_dir' in row else None ) - blocks.append(block) - return blocks + # temporarily disable wiregrid time const measurements + if wiregrid_target.name == 'wiregrid_gain': + wiregrid_targets.append(wiregrid_target) + + return wiregrid_targets def parse_sequence_from_toast_lat(ifile): """ diff --git a/src/schedlib/policies/lat.py b/src/schedlib/policies/lat.py index edfe900..a5c7919 100644 --- a/src/schedlib/policies/lat.py +++ b/src/schedlib/policies/lat.py @@ -18,7 +18,8 @@ from .stages import get_build_stage from .stages.build_op import get_parking from . import tel -from .tel import State, CalTarget, make_blocks +from .tel import State, make_blocks +from ..instrument import CalTarget logger = u.init_logger(__name__) diff --git a/src/schedlib/policies/sat.py b/src/schedlib/policies/sat.py index 1ddd122..9981dae 100644 --- a/src/schedlib/policies/sat.py +++ b/src/schedlib/policies/sat.py @@ -4,7 +4,7 @@ import numpy as np import yaml import os.path as op -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from dataclasses_json import dataclass_json import datetime as dt @@ -18,14 +18,14 @@ from .stages import get_build_stage from .stages.build_op import get_parking from . import tel -from ..instrument import CalTarget +from ..instrument import CalTarget, WiregridTarget + logger = u.init_logger(__name__) HWP_SPIN_UP = 7*u.minute HWP_SPIN_DOWN = 15*u.minute BORESIGHT_DURATION = 1*u.minute -WIREGRID_DURATION = 15*u.minute COMMANDS_HWP_BRAKE = [ "run.smurf.stream('on', subtype='cal', tag='hwp_spin_down')", @@ -69,13 +69,6 @@ class State(tel.State): def get_boresight(self): return self.boresight_rot_now -@dataclass(frozen=True) -class WiregridTarget: - hour: int - el_target: float - az_target: float = 180 - duration: float = WIREGRID_DURATION - class SchedMode(tel.SchedMode): """ Enumerate different options for scheduling operations in SATPolicy. @@ -85,6 +78,7 @@ class SchedMode(tel.SchedMode): Wiregrid : str 'wiregrid'; Wiregrid observations scheduled between block.t0 and block.t1 """ + PreWiregrid = 'pre_wiregrid' Wiregrid = 'wiregrid' @@ -329,11 +323,23 @@ def setup_boresight(state, block, apply_boresight_rot=True, brake_hwp=True, cryo def bias_step(state, block, bias_step_cadence=None): return tel.bias_step(state, block, bias_step_cadence) -@cmd.operation(name='sat.wiregrid', duration=WIREGRID_DURATION) -def wiregrid(state): - return state, [ - "run.wiregrid.calibrate(continuous=False, elevation_check=True, boresight_check=False, temperature_check=False)" - ] +@cmd.operation(name='sat.wiregrid', return_duration=True) +def wiregrid(state, block, min_wiregrid_el=47.5): + assert state.hwp_spinning == True, "hwp is not spinning" + assert block.alt >= min_wiregrid_el, f"Block {block} is below the minimum wiregrid elevation of {min_wiregrid_el} degrees." + + if block.name == 'wiregrid_gain': + return state, (block.t1 - state.curr_time).total_seconds(), [ + "run.wiregrid.calibrate(continuous=False, elevation_check=True, boresight_check=False, temperature_check=False)" + ] + # elif block.name == 'wiregrid_time_const': + # # wiregrid time const reverses the hwp direction + # state = state.replace(hwp_dir=not state.hwp_dir) + # return state, (block.t1 - state.curr_time).total_seconds(), [ + # f"# hwp spinning with forward={not state.hwp_dir}", + # "run.wiregrid.calibrate(continuous=False, elevation_check=True, boresight_check=False, temperature_check=False)", + # f"# hwp spinning with forward={state.hwp_dir}" + # ] @cmd.operation(name="move_to", return_duration=True) def move_to(state, az, el, az_offset=0, el_offset=0, min_el=48, brake_hwp=True, force=False): @@ -365,15 +371,24 @@ class SATPolicy(tel.TelPolicy): and False is reverse. brake_hwp : bool a bool that specifies whether or not active braking should be used for the hwp. + disable_hwp : bool + a bool that specifies whether or not to disable the hwp entirely. min_hwp_el : float the minimum elevation a move command to go to without stopping the hwp first boresight_override : float the angle of boresight to use if not None + wiregrid_az : float + azimuth to use for wiregrid measurements + wiregrid_el : float + elevation to use for wiregrid measurements """ hwp_override: Optional[bool] = None brake_hwp: Optional[bool] = True + disable_hwp: bool = False min_hwp_el: float = 48 # deg boresight_override: Optional[float] = None + wiregrid_az: float = 180 + wiregrid_el: float = 48 def apply_overrides(self, blocks): if self.boresight_override is not None: @@ -414,40 +429,51 @@ def from_config(cls, config: Union[Dict[str, Any], str]): config = yaml.load(config, Loader=loader) return cls(**config) - def divide_blocks(self, block, max_dt=dt.timedelta(minutes=60), min_dt=dt.timedelta(minutes=15)): - duration = block.duration - - # if the block is small enough, return it as is - if duration <= (max_dt + min_dt): - return [block] - - n_blocks = duration // max_dt - remainder = duration % max_dt - - # split if 1 block with remainder > min duration - if n_blocks == 1: - return core.block_split(block, block.t0 + max_dt) - - blocks = [] - # calculate the offset for splitting - offset = (remainder + max_dt) / 2 if remainder.total_seconds() > 0 else max_dt - - split_blocks = core.block_split(block, block.t0 + offset) - blocks.append(split_blocks[0]) - - # split the remaining block into chunks of max duration - for i in range(n_blocks - 1): - split_blocks = core.block_split(split_blocks[-1], split_blocks[-1].t0 + max_dt) - blocks.append(split_blocks[0]) + def make_source_scans(self, target, blocks, sun_rule): + # digest array_query: it could be a fnmatch pattern matching the path + # in the geometry dict, or it could be looked up from a predefined + # wafer_set dict. Here we account for the latter case: + # look up predefined query in wafer_set + if target.array_query in self.wafer_sets: + array_query = self.wafer_sets[target.array_query] + else: + array_query = target.array_query + + # build array geometry information based on the query + array_info = inst.array_info_from_query(self.geometries, array_query) + logger.debug(f"-> array_info: {array_info}") + + # apply MakeCESourceScan rule to transform known observing windows into + # actual scan blocks + rule = ru.MakeCESourceScan( + array_info=array_info, + el_bore=target.el_bore, + drift=target.drift, + boresight_rot=target.boresight_rot, + allow_partial=target.allow_partial, + az_branch=target.az_branch, + source_direction=target.source_direction, + ) + source_scans = rule(blocks['calibration'][target.source]) + + # sun check again: previous sun check ensure source is not too + # close to the sun, but our scan may still get close enough to + # the sun, in which case we will trim it or delete it depending + # on whether allow_partial is True + if target.allow_partial: + logger.info("-> allow_partial = True: trimming scan options by sun rule") + min_dur_rule = ru.make_rule('min-duration', **self.rules['min-duration']) + source_scans = min_dur_rule(sun_rule(source_scans)) + else: + logger.info("-> allow_partial = False: filtering scan options by sun rule") + source_scans = core.seq_filter(lambda b: b == sun_rule(b), source_scans) - # add the remaining part - if remainder.total_seconds() > 0: - split_blocks = core.block_split(split_blocks[-1], split_blocks[-1].t0 + offset) - blocks.append(split_blocks[0]) + # flatten and sort + source_scans = core.seq_sort(source_scans, flatten=True) - return blocks + return source_scans - def init_seqs(self, t0: dt.datetime, t1: dt.datetime) -> core.BlocksTree: + def init_cmb_seqs(self, t0: dt.datetime, t1: dt.datetime) -> core.BlocksTree: """ Initialize the sequences for the scheduler to process. @@ -470,33 +496,6 @@ def init_seqs(self, t0: dt.datetime, t1: dt.datetime) -> core.BlocksTree: is_leaf=lambda x: isinstance(x, dict) and 'type' in x ) - # by default add calibration blocks specified in cal_targets if not already specified - for cal_target in self.cal_targets: - if isinstance(cal_target, CalTarget): - source = cal_target.source - if source not in blocks['calibration']: - blocks['calibration'][source] = src.source_gen_seq(source, t0, t1) - elif isinstance(cal_target, WiregridTarget): - wiregrid_candidates = [] - current_date = t0.date() - end_date = t1.date() - - while current_date <= end_date: - candidate_time = dt.datetime.combine(current_date, dt.time(cal_target.hour, 0), tzinfo=dt.timezone.utc) - if t0 <= candidate_time <= t1: - wiregrid_candidates.append( - inst.StareBlock( - name='wiregrid', - t0=candidate_time, - t1=candidate_time + dt.timedelta(seconds=cal_target.duration), - az=cal_target.az_target, - alt=cal_target.el_target, - subtype='wiregrid' - ) - ) - current_date += dt.timedelta(days=1) - blocks['calibration']['wiregrid'] = wiregrid_candidates - # trim to given time range blocks = core.seq_trim(blocks, t0, t1) @@ -521,10 +520,10 @@ def apply(self, blocks: core.BlocksTree) -> core.BlocksTree: Returns ------- - BlocksTree + blocks : BlocksTree New blocks tree after applying the specified observing rules. - """ + # ----------------------------------------------------------------- # step 1: preliminary sun avoidance # - get rid of source observing windows too close to the sun @@ -539,6 +538,12 @@ def apply(self, blocks: core.BlocksTree) -> core.BlocksTree: logger.error("no sun avoidance rule specified!") raise ValueError("Sun rule is required!") + # min duration rule + if 'min-duration' in self.rules: + # initial min duration rule to remove edge cases of very short scans + min_dur_rule = ru.make_rule('min-duration', **self.rules['min-duration']) + blocks['calibration'] = min_dur_rule(blocks['calibration']) + # ----------------------------------------------------------------- # step 2: plan calibration scans # - refer to each target specified in cal_targets @@ -548,57 +553,16 @@ def apply(self, blocks: core.BlocksTree) -> core.BlocksTree: logger.info("planning calibration scans...") cal_blocks = [] + saved_cal_targets = [] for target in self.cal_targets: logger.info(f"-> planning calibration scans for {target}...") if isinstance(target, WiregridTarget): - logger.info(f"-> planning wiregrid scans for {target}...") - cal_blocks += core.seq_map(lambda b: b.replace(subtype='wiregrid'), - blocks['calibration']['wiregrid']) continue assert target.source in blocks['calibration'], f"source {target.source} not found in sequence" - # digest array_query: it could be a fnmatch pattern matching the path - # in the geometry dict, or it could be looked up from a predefined - # wafer_set dict. Here we account for the latter case: - # look up predefined query in wafer_set - if target.array_query in self.wafer_sets: - array_query = self.wafer_sets[target.array_query] - else: - array_query = target.array_query - - # build array geometry information based on the query - array_info = inst.array_info_from_query(self.geometries, array_query) - logger.debug(f"-> array_info: {array_info}") - - # apply MakeCESourceScan rule to transform known observing windows into - # actual scan blocks - rule = ru.MakeCESourceScan( - array_info=array_info, - el_bore=target.el_bore, - drift=target.drift, - boresight_rot=target.boresight_rot, - allow_partial=target.allow_partial, - az_branch=target.az_branch, - source_direction=target.source_direction, - ) - source_scans = rule(blocks['calibration'][target.source]) - - # sun check again: previous sun check ensure source is not too - # close to the sun, but our scan may still get close enough to - # the sun, in which case we will trim it or delete it depending - # on whether allow_partial is True - if target.allow_partial: - logger.info("-> allow_partial = True: trimming scan options by sun rule") - min_dur_rule = ru.make_rule('min-duration', **self.rules['min-duration']) - source_scans = min_dur_rule(sun_rule(source_scans)) - else: - logger.info("-> allow_partial = False: filtering scan options by sun rule") - source_scans = core.seq_filter(lambda b: b == sun_rule(b), source_scans) - - # flatten and sort - source_scans = core.seq_sort(source_scans, flatten=True) + source_scans = self.make_source_scans(target, blocks, sun_rule) if len(source_scans) == 0: logger.warning(f"-> no scan options available for {target.source} ({target.array_query})") @@ -633,13 +597,10 @@ def apply(self, blocks: core.BlocksTree) -> core.BlocksTree: ) cal_blocks.append(cal_block) - blocks['calibration'] = cal_blocks + blocks['calibration'] = cal_blocks + blocks['calibration']['wiregrid'] logger.info(f"-> after calibration policy: {u.pformat(blocks['calibration'])}") - # check sun avoidance again - blocks['calibration'] = core.seq_flatten(sun_rule(blocks['calibration'])) - # min duration rule if 'min-duration' in self.rules: logger.info(f"applying min duration rule: {self.rules['min-duration']}") @@ -658,7 +619,7 @@ def apply(self, blocks: core.BlocksTree) -> core.BlocksTree: # add proper subtypes blocks['calibration'] = core.seq_map( - lambda block: block.replace(subtype="cal") if block.name != 'wiregrid' else block, + lambda block: block.replace(subtype="cal") if block.subtype != 'wiregrid' else block, blocks['calibration'] ) @@ -691,20 +652,17 @@ def apply(self, blocks: core.BlocksTree) -> core.BlocksTree: # add hwp direction to cal blocks if self.hwp_override is None: for i, block in enumerate(blocks): - if block.subtype=='cal' and block.hwp_dir is not None: - # try next blocks - for j in range(1, len(blocks)-i): - if blocks[i+j].subtype=="cmb": - blocks[i] = block.replace(hwp_dir=blocks[i+j].hwp_dir) - break + if (block.subtype=='cal' or block.subtype=='wiregrid') and block.hwp_dir is None: + candidates = [cmb_block for cmb_block in blocks if cmb_block.subtype == "cmb" and cmb_block.t0 < block.t0] + if candidates: + cmb_block = max(candidates, key=lambda x: x.t0) else: - # try previous blocks - for j in range(1, i+1): - if blocks[i-j].subtype=="cmb": - blocks[i] = block.replace(hwp_dir=blocks[i-j].hwp_dir) - break + candidates = [cmb_block for cmb_block in blocks if cmb_block.subtype == "cmb" and cmb_block.t0 > block.t0] + if candidates: + cmb_block = min(candidates, key=lambda x: x.t0) else: raise ValueError(f"Cannot assign HWP direction to cal block {block}") + blocks[i] = block.replace(hwp_dir=cmb_block.hwp_dir) # ----------------------------------------------------------------- # step 5: verify @@ -804,6 +762,7 @@ def seq2cmd( # first resolve overlapping between cal and cmb cal_blocks = core.seq_flatten(core.seq_filter(lambda b: b.subtype == 'cal', seq)) cmb_blocks = core.seq_flatten(core.seq_filter(lambda b: b.subtype == 'cmb', seq)) + wiregrid_blocks = core.seq_flatten(core.seq_filter(lambda b: b.subtype == 'wiregrid', seq)) cal_blocks += wiregrid_blocks seq = core.seq_sort(core.seq_merge(cmb_blocks, cal_blocks, flatten=True)) @@ -821,6 +780,7 @@ def seq2cmd( cmb_post = [op for op in self.operations if op['sched_mode'] == SchedMode.PostObs] pre_sess = [op for op in self.operations if op['sched_mode'] == SchedMode.PreSession] pos_sess = [op for op in self.operations if op['sched_mode'] == SchedMode.PostSession] + wiregrid_pre = [op for op in self.operations if op['sched_mode'] == SchedMode.PreWiregrid] wiregrid_in = [op for op in self.operations if op['sched_mode'] == SchedMode.Wiregrid] def map_block(block): @@ -831,7 +791,7 @@ def map_block(block): 'pre': cal_pre, 'in': cal_in, 'post': cal_post, - 'priority': block.priority + 'priority': -1 } elif block.subtype == 'cmb': return { @@ -846,10 +806,10 @@ def map_block(block): return { 'name': block.name, 'block': block, - 'pre': [], + 'pre': wiregrid_pre, 'in': wiregrid_in, 'post': [], - 'priority': block.priority + 'priority': -1 } else: raise ValueError(f"unexpected block subtype: {block.subtype}") @@ -866,7 +826,7 @@ def map_block(block): 'pre': [], 'in': [], 'post': pre_sess, # scheduled after t0 - 'priority': 0, + 'priority': -1, 'pinned': True # remain unchanged during multi-pass } @@ -897,7 +857,7 @@ def map_block(block): 'pre': pos_sess, # scheduled before t1 'in': [], 'post': [], - 'priority': 0, + 'priority': -1, 'pinned': True # remain unchanged during multi-pass } seq = [start_block] + seq + [end_block] @@ -910,8 +870,8 @@ def map_block(block): def add_cal_target(self, *args, **kwargs): self.cal_targets.append(make_cal_target(*args, **kwargs)) - def add_wiregrid_target(self, el_target, hour_utc=12, az_target=180, duration=WIREGRID_DURATION, **kwargs): - self.cal_targets.append(WiregridTarget(hour=hour_utc, az_target=az_target, el_target=el_target, duration=duration)) + def add_wiregrid_target(self, el_target, hour_utc=12, az_target=180, **kwargs): + self.cal_targets.append(WiregridTarget(hour=hour_utc, az_target=az_target, el_target=el_target)) # ------------------------ # utilities diff --git a/src/schedlib/policies/satp1.py b/src/schedlib/policies/satp1.py index a7b8e8e..8c8f22c 100644 --- a/src/schedlib/policies/satp1.py +++ b/src/schedlib/policies/satp1.py @@ -1,12 +1,15 @@ import numpy as np -from dataclasses import dataclass +from dataclasses import dataclass, replace import datetime as dt from typing import Optional +from ..thirdparty import SunAvoidance +from .. import config as cfg, core, source as src, rules as ru from .. import source as src, utils as u -from .sat import SATPolicy, State, SchedMode, WiregridTarget, make_geometry +from .sat import SATPolicy, State, SchedMode, make_geometry from .tel import make_blocks, CalTarget +from ..instrument import WiregridTarget, StareBlock, parse_wiregrid_targets_from_file logger = u.init_logger(__name__) @@ -82,6 +85,7 @@ def make_cal_target( az_speed=az_speed, az_accel=az_accel, source_direction=source_direction, + from_table=False ) @@ -146,9 +150,14 @@ def make_operations( { 'name': 'sat.wrap_up' , 'sched_mode': SchedMode.PostSession}, ] - wiregrid_ops = [ - { 'name': 'sat.wiregrid', 'sched_mode': SchedMode.Wiregrid } - ] + wiregrid_ops = [] + if not disable_hwp: + wiregrid_ops += [ + { 'name': 'sat.setup_boresight' , 'sched_mode': SchedMode.PreCal, 'apply_boresight_rot': apply_boresight_rot, 'brake_hwp': brake_hwp, 'cryo_stabilization_time': cryo_stabilization_time}, + { 'name': 'sat.det_setup' , 'sched_mode': SchedMode.PreWiregrid, 'apply_boresight_rot': apply_boresight_rot, 'iv_cadence':iv_cadence,}, + { 'name': 'sat.hwp_spin_up' , 'sched_mode': SchedMode.PreWiregrid, 'disable_hwp': disable_hwp, 'brake_hwp': brake_hwp}, + { 'name': 'sat.wiregrid', 'sched_mode': SchedMode.Wiregrid } + ] return pre_session_ops + cal_ops + cmb_ops + post_session_ops + wiregrid_ops def make_config( @@ -170,7 +179,13 @@ def make_config( boresight_override=None, hwp_override=None, brake_hwp=True, + disable_hwp=False, az_motion_override=False, + az_branch_override=None, + allow_partial_override=False, + drift_override=True, + wiregrid_az=180, + wiregrid_el=48, **op_cfg ): blocks = make_blocks(master_file, 'sat-cmb') @@ -182,7 +197,7 @@ def make_config( az_speed, az_accel, iv_cadence, bias_step_cadence, det_setup_duration, - brake_hwp, + brake_hwp, disable_hwp, **op_cfg ) @@ -229,6 +244,7 @@ def make_config( 'boresight_override': boresight_override, 'hwp_override': hwp_override, 'brake_hwp': brake_hwp, + 'disable_hwp': disable_hwp, 'az_motion_override': az_motion_override, 'az_speed': az_speed, 'az_accel': az_accel, @@ -238,6 +254,11 @@ def make_config( 'bias_step_cadence': bias_step_cadence, 'min_hwp_el': min_hwp_el, 'max_cmb_scan_duration': max_cmb_scan_duration, + 'az_branch_override': az_branch_override, + 'allow_partial_override': allow_partial_override, + 'drift_override': drift_override, + 'wiregrid_az': wiregrid_az, + 'wiregrid_el': wiregrid_el, 'stages': { 'build_op': { 'plan_moves': { @@ -282,7 +303,13 @@ def from_defaults(cls, boresight_override=None, hwp_override=None, brake_hwp=True, + disable_hwp=False, az_motion_override=False, + az_branch_override=None, + allow_partial_override=False, + drift_override=True, + wiregrid_az=180, + wiregrid_el=48, **op_cfg ): if cal_targets is None: @@ -306,7 +333,13 @@ def from_defaults(cls, boresight_override, hwp_override, brake_hwp, + disable_hwp, az_motion_override, + az_branch_override, + allow_partial_override, + drift_override, + wiregrid_az, + wiregrid_el, **op_cfg )) return x @@ -314,6 +347,57 @@ def from_defaults(cls, def add_cal_target(self, *args, **kwargs): self.cal_targets.append(make_cal_target(*args, **kwargs)) + def init_cal_seqs(self, wgfile, blocks, t0, t1, anchor_time=None): + # get wiregrid file + if wgfile is not None and not self.disable_hwp: + wiregrid_candidates = parse_wiregrid_targets_from_file(wgfile) + # since we don't restrict the wiregrid observations, make sure they end before t1 + wiregrid_candidates[:] = [wiregrid_candidate for wiregrid_candidate in wiregrid_candidates if wiregrid_candidate.t0 >= t0 and wiregrid_candidate.t1 <= t1] + + for i, wiregrid_candidate in enumerate(wiregrid_candidates): + candidates = [block for block in blocks['baseline']['cmb'] if block.t0 < wiregrid_candidate.t0] + if candidates: + block = max(candidates, key=lambda x: x.t0) + else: + candidates = [block for block in blocks['baseline']['cmb'] if block.t0 > wiregrid_candidate.t0] + if candidates: + block = min(candidates, key=lambda x: x.t0) + else: + raise ValueError("Cannot find nearby block for cal target") + + if self.boresight_override is None: + wiregrid_candidates[i] = replace(wiregrid_candidates[i], boresight_rot=block.boresight_angle) + else: + wiregrid_candidates[i] = replace(wiregrid_candidates[i], boresight_rot=self.boresight_override) + + self.cal_targets += wiregrid_candidates + + wiregrid_candidates = [] + + # by default add calibration blocks specified in cal_targets if not already specified + for cal_target in self.cal_targets: + if isinstance(cal_target, CalTarget): + source = cal_target.source + if source not in blocks['calibration']: + blocks['calibration'][source] = src.source_gen_seq(source, t0, t1) + elif isinstance(cal_target, WiregridTarget): + wiregrid_candidates.append( + StareBlock( + name=cal_target.name, + t0=cal_target.t0, + t1=cal_target.t1, + az=self.wiregrid_az, + alt=self.wiregrid_el, + tag=cal_target.tag, + subtype='wiregrid', + hwp_dir=self.hwp_override if self.hwp_override is not None else None, + boresight_angle=cal_target.boresight_rot + ) + ) + blocks['calibration']['wiregrid'] = wiregrid_candidates + + return blocks + def init_state(self, t0: dt.datetime) -> State: """customize typical initial state for satp1, if needed""" if self.state_file is not None: diff --git a/src/schedlib/policies/satp2.py b/src/schedlib/policies/satp2.py index ced9bfe..5c8dae0 100644 --- a/src/schedlib/policies/satp2.py +++ b/src/schedlib/policies/satp2.py @@ -1,12 +1,15 @@ import numpy as np -from dataclasses import dataclass +from dataclasses import dataclass, replace import datetime as dt from typing import Optional +from ..thirdparty import SunAvoidance +from .. import config as cfg, core, source as src, rules as ru from .. import source as src, utils as u from .sat import SATPolicy, State, SchedMode, make_geometry from .tel import make_blocks, CalTarget +from ..instrument import WiregridTarget, StareBlock, parse_wiregrid_targets_from_file logger = u.init_logger(__name__) @@ -83,6 +86,7 @@ def make_cal_target( az_speed=az_speed, az_accel=az_accel, source_direction=source_direction, + from_table=False ) def make_operations( @@ -146,7 +150,15 @@ def make_operations( { 'name': 'sat.wrap_up' , 'sched_mode': SchedMode.PostSession}, ] - return pre_session_ops + cal_ops + cmb_ops + post_session_ops + wiregrid_ops = [] + if not disable_hwp: + wiregrid_ops += [ + { 'name': 'sat.setup_boresight' , 'sched_mode': SchedMode.PreCal, 'apply_boresight_rot': apply_boresight_rot, 'brake_hwp': brake_hwp, 'cryo_stabilization_time': cryo_stabilization_time}, + { 'name': 'sat.det_setup' , 'sched_mode': SchedMode.PreWiregrid, 'apply_boresight_rot': apply_boresight_rot, 'iv_cadence':iv_cadence,}, + { 'name': 'sat.hwp_spin_up' , 'sched_mode': SchedMode.PreWiregrid, 'disable_hwp': disable_hwp, 'brake_hwp': brake_hwp}, + { 'name': 'sat.wiregrid', 'sched_mode': SchedMode.Wiregrid } + ] + return pre_session_ops + cal_ops + cmb_ops + post_session_ops + wiregrid_ops def make_config( master_file, @@ -167,7 +179,13 @@ def make_config( boresight_override=None, hwp_override=None, brake_hwp=True, + disable_hwp=False, az_motion_override=False, + az_branch_override=None, + allow_partial_override=False, + drift_override=True, + wiregrid_az=180, + wiregrid_el=48, **op_cfg ): blocks = make_blocks(master_file, 'sat-cmb') @@ -180,6 +198,7 @@ def make_config( iv_cadence, bias_step_cadence, det_setup_duration, brake_hwp, + disable_hwp, **op_cfg ) @@ -226,6 +245,7 @@ def make_config( 'boresight_override': boresight_override, 'hwp_override': hwp_override, 'brake_hwp': brake_hwp, + 'disable_hwp': disable_hwp, 'az_motion_override': az_motion_override, 'az_speed' : az_speed, 'az_accel' : az_accel, @@ -235,6 +255,11 @@ def make_config( 'bias_step_cadence' : bias_step_cadence, 'min_hwp_el' : min_hwp_el, 'max_cmb_scan_duration' : max_cmb_scan_duration, + 'az_branch_override': az_branch_override, + 'allow_partial_override': allow_partial_override, + 'drift_override': drift_override, + 'wiregrid_az': wiregrid_az, + 'wiregrid_el': wiregrid_el, 'stages': { 'build_op': { 'plan_moves': { @@ -279,7 +304,13 @@ def from_defaults(cls, boresight_override=None, hwp_override=None, brake_hwp=True, + disable_hwp=False, az_motion_override=False, + az_branch_override=None, + allow_partial_override=False, + drift_override=True, + wiregrid_az=180, + wiregrid_el=48, **op_cfg ): if cal_targets is None: @@ -304,7 +335,13 @@ def from_defaults(cls, boresight_override, hwp_override, brake_hwp, + disable_hwp, az_motion_override, + az_branch_override, + allow_partial_override, + drift_override, + wiregrid_az, + wiregrid_el, **op_cfg )) return x @@ -312,6 +349,56 @@ def from_defaults(cls, def add_cal_target(self, *args, **kwargs): self.cal_targets.append(make_cal_target(*args, **kwargs)) + def init_cal_seqs(self, wgfile, blocks, t0, t1, anchor_time=None): + # get wiregrid file + if wgfile is not None and not self.disable_hwp: + wiregrid_candidates = parse_wiregrid_targets_from_file(wgfile) + wiregrid_candidates[:] = [wiregrid_candidate for wiregrid_candidate in wiregrid_candidates if wiregrid_candidate.t0 >= t0 and wiregrid_candidate.t1 <= t1] + + for i, wiregrid_candidate in enumerate(wiregrid_candidates): + candidates = [block for block in blocks['baseline']['cmb'] if block.t0 < wiregrid_candidate.t0] + if candidates: + block = max(candidates, key=lambda x: x.t0) + else: + candidates = [block for block in blocks['baseline']['cmb'] if block.t0 > wiregrid_candidate.t0] + if candidates: + block = min(candidates, key=lambda x: x.t0) + else: + raise ValueError("Cannot find nearby block for cal target") + + if self.boresight_override is None: + wiregrid_candidates[i] = replace(wiregrid_candidates[i], boresight_rot=block.boresight_angle) + else: + wiregrid_candidates[i] = replace(wiregrid_candidates[i], boresight_rot=self.boresight_override) + + self.cal_targets += wiregrid_candidates + + wiregrid_candidates = [] + + # by default add calibration blocks specified in cal_targets if not already specified + for cal_target in self.cal_targets: + if isinstance(cal_target, CalTarget): + source = cal_target.source + if source not in blocks['calibration']: + blocks['calibration'][source] = src.source_gen_seq(source, t0, t1) + elif isinstance(cal_target, WiregridTarget): + wiregrid_candidates.append( + StareBlock( + name=cal_target.name, + t0=cal_target.t0, + t1=cal_target.t1, + az=self.wiregrid_az, + alt=self.wiregrid_el, + tag=cal_target.tag, + subtype='wiregrid', + hwp_dir=self.hwp_override if self.hwp_override is not None else None, + boresight_angle=cal_target.boresight_rot + ) + ) + blocks['calibration']['wiregrid'] = wiregrid_candidates + + return blocks + def init_state(self, t0: dt.datetime) -> State: """customize typical initial state for satp2, if needed""" if self.state_file is not None: diff --git a/src/schedlib/policies/satp3.py b/src/schedlib/policies/satp3.py index 1264b80..30eb851 100644 --- a/src/schedlib/policies/satp3.py +++ b/src/schedlib/policies/satp3.py @@ -1,10 +1,15 @@ import numpy as np -from dataclasses import dataclass +from dataclasses import dataclass, replace import datetime as dt +from typing import Optional + +from ..thirdparty import SunAvoidance +from .. import config as cfg, core, source as src, rules as ru from .. import source as src, utils as u from .sat import SATPolicy, State, SchedMode, make_geometry from .tel import make_blocks, CalTarget +from ..instrument import WiregridTarget, StareBlock, parse_wiregrid_targets_from_file logger = u.init_logger(__name__) @@ -62,6 +67,7 @@ def make_cal_target( az_speed=az_speed, az_accel=az_accel, source_direction=source_direction, + from_table=False ) @@ -152,9 +158,13 @@ def make_operations( { 'name': 'sat.wrap_up' , 'sched_mode': SchedMode.PostSession}, ] - wiregrid_ops = [ - { 'name': 'sat.wiregrid', 'sched_mode': SchedMode.Wiregrid } - ] + wiregrid_ops = [] + if not disable_hwp: + wiregrid_ops += [ + { 'name': 'sat.det_setup' , 'sched_mode': SchedMode.PreWiregrid, 'apply_boresight_rot': apply_boresight_rot, 'iv_cadence':iv_cadence,}, + { 'name': 'sat.hwp_spin_up' , 'sched_mode': SchedMode.PreWiregrid, 'disable_hwp': disable_hwp, 'brake_hwp': brake_hwp}, + { 'name': 'sat.wiregrid', 'sched_mode': SchedMode.Wiregrid } + ] return pre_session_ops + cal_ops + cmb_ops + post_session_ops + wiregrid_ops def make_config( @@ -176,7 +186,13 @@ def make_config( boresight_override=None, hwp_override=None, brake_hwp=True, + disable_hwp=False, az_motion_override=False, + az_branch_override=None, + allow_partial_override=False, + drift_override=True, + wiregrid_az=180, + wiregrid_el=48, **op_cfg ): blocks = make_blocks(master_file, 'sat-cmb') @@ -189,6 +205,7 @@ def make_config( iv_cadence, bias_step_cadence, det_setup_duration, brake_hwp, + disable_hwp, **op_cfg ) @@ -239,6 +256,7 @@ def make_config( 'boresight_override': boresight_override, 'hwp_override': hwp_override, 'brake_hwp': brake_hwp, + 'disable_hwp': disable_hwp, 'az_motion_override': az_motion_override, 'az_speed': az_speed, 'az_accel': az_accel, @@ -248,6 +266,11 @@ def make_config( 'bias_step_cadence': bias_step_cadence, 'min_hwp_el': min_hwp_el, 'max_cmb_scan_duration': max_cmb_scan_duration, + 'az_branch_override': az_branch_override, + 'allow_partial_override': allow_partial_override, + 'drift_override': drift_override, + 'wiregrid_az': wiregrid_az, + 'wiregrid_el': wiregrid_el, 'stages': { 'build_op': { 'plan_moves': { @@ -292,7 +315,13 @@ def from_defaults(cls, boresight_override=None, hwp_override=None, brake_hwp=True, + disable_hwp=False, az_motion_override=False, + az_branch_override=None, + allow_partial_override=False, + drift_override=True, + wiregrid_az=180, + wiregrid_el=48, **op_cfg ): if cal_targets is None: @@ -316,7 +345,13 @@ def from_defaults(cls, boresight_override, hwp_override, brake_hwp, + disable_hwp, az_motion_override, + az_branch_override, + allow_partial_override, + drift_override, + wiregrid_az, + wiregrid_el, **op_cfg) ) @@ -325,6 +360,39 @@ def from_defaults(cls, def add_cal_target(self, *args, **kwargs): self.cal_targets.append(make_cal_target(*args, **kwargs)) + def init_cal_seqs(self, wgfile, blocks, t0, t1, anchor_time=None): + # get wiregrid file + if wgfile is not None and not self.disable_hwp: + wiregrid_candidates = parse_wiregrid_targets_from_file(wgfile) + wiregrid_candidates[:] = [wiregrid_candidate for wiregrid_candidate in wiregrid_candidates if wiregrid_candidate.t0 >= t0 and wiregrid_candidate.t1 <= t1] + self.cal_targets += wiregrid_candidates + + wiregrid_candidates = [] + + # by default add calibration blocks specified in cal_targets if not already specified + for cal_target in self.cal_targets: + if isinstance(cal_target, CalTarget): + source = cal_target.source + if source not in blocks['calibration']: + blocks['calibration'][source] = src.source_gen_seq(source, t0, t1) + elif isinstance(cal_target, WiregridTarget): + wiregrid_candidates.append( + StareBlock( + name=cal_target.name, + t0=cal_target.t0, + t1=cal_target.t1, + az=self.wiregrid_az, + alt=self.wiregrid_el, + tag=cal_target.tag, + subtype='wiregrid', + hwp_dir=self.hwp_override if self.hwp_override is not None else None + ) + ) + blocks['calibration']['wiregrid'] = wiregrid_candidates + + return blocks + + def init_state(self, t0: dt.datetime) -> State: """customize typical initial state for satp3, if needed""" if self.state_file is not None: diff --git a/src/schedlib/policies/tel.py b/src/schedlib/policies/tel.py index 4ec4d8b..2207c0d 100644 --- a/src/schedlib/policies/tel.py +++ b/src/schedlib/policies/tel.py @@ -471,7 +471,15 @@ def divide_blocks(self, block, max_dt=dt.timedelta(minutes=60), min_dt=dt.timede # split if 1 block with remainder > min duration if n_blocks == 1: - return core.block_split(block, block.t0 + max_dt) + blocks = core.block_split(block, block.t0 + max_dt) + for i, b in enumerate(blocks): + tags = b.tag.split(',') + for j, item in enumerate(tags): + if item.startswith('uid'): + tags[j] = item + '-pass-' + str(i) + break + blocks[i] = blocks[i].replace(tag=",".join(tags)) + return blocks#core.block_split(block, block.t0 + max_dt) blocks = [] # calculate the offset for splitting @@ -490,6 +498,14 @@ def divide_blocks(self, block, max_dt=dt.timedelta(minutes=60), min_dt=dt.timede split_blocks = core.block_split(split_blocks[-1], split_blocks[-1].t0 + offset) blocks.append(split_blocks[0]) + for i, b in enumerate(blocks): + tags = b.tag.split(',') + for j, item in enumerate(tags): + if item.startswith('uid'): + tags[j] = item + '-pass-' + str(i) + break + blocks[i] = blocks[i].replace(tag=",".join(tags)) + return blocks def cmd2txt(self, irs, t0, t1, state=None): diff --git a/src/schedlib/utils.py b/src/schedlib/utils.py index 82d87e9..462e682 100644 --- a/src/schedlib/utils.py +++ b/src/schedlib/utils.py @@ -1,5 +1,5 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import datetime, timezone, date import pandas as pd import numpy as np from functools import reduce @@ -16,8 +16,15 @@ sidereal_day = 0.997269566 * day deg = np.pi / 180 +def get_cycle_option(t, options, anchor=None): + if anchor is None: + anchor = str2datetime("1970-01-01T00:00:00+00:00") + delta_days = (t - anchor).days + index = delta_days % len(options) + return index + def str2ctime(time_str): - ctime = (pd.Timestamp(time_str) - pd.Timestamp("1970-01-01")) // pd.Timedelta('1s') + ctime = (pd.Timestamp(time_str).tz_localize(None) - pd.Timestamp("1970-01-01")) // pd.Timedelta('1s') return ctime def str2datetime(time_str): @@ -189,11 +196,11 @@ def wrapper(obj): def path2key(path, ignore_seqkey=False): """convert a path (used in tree_util.tree_map_with_path) to a dot-separated key - + Parameters ---------- path: a list of SequenceKey or DictKey - ignore_array: if True, ignore the SequencyKey index in the path + ignore_array: if True, ignore the SequencyKey index in the path Returns ------- @@ -213,19 +220,19 @@ def path2key(path, ignore_seqkey=False): def match_query(path, query): """in order for a query to match with a path, it can - satisfy the following: + satisfy the following: 1. the query is a substring of the path 2. the query is a glob pattern that matches the path - 3. if the query is a comma-separated list of multiple queries, + 3. if the query is a comma-separated list of multiple queries, any of them meeting comdition 1 and 2 will return True """ key = path2key(path) # first match the constraint to key queires = query.split(",") for q in queires: - if q in key: + if q in key: return True - if fnmatch.fnmatch(key, q): + if fnmatch.fnmatch(key, q): return True return False