From 648cb8ba3a8959c122fe97bafa3dc3f4e1b68987 Mon Sep 17 00:00:00 2001 From: LorenzzoQM Date: Fri, 23 Oct 2026 16:31:14 -0600 Subject: [PATCH 1/4] [#363] Add hybrid action option --- src/bsk_rl/act/discrete_actions.py | 346 ++++++++++++++++++++++----- src/bsk_rl/act/rso_imaging.py | 4 +- src/bsk_rl/gym.py | 81 +++++-- src/bsk_rl/sats/access_satellite.py | 38 ++- src/bsk_rl/sim/fsw/ground_imaging.py | 66 ++++- 5 files changed, 447 insertions(+), 88 deletions(-) diff --git a/src/bsk_rl/act/discrete_actions.py b/src/bsk_rl/act/discrete_actions.py index 6cd15a8e..66a87c94 100644 --- a/src/bsk_rl/act/discrete_actions.py +++ b/src/bsk_rl/act/discrete_actions.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union import numpy as np +from Basilisk.utilities import macros as mc from gymnasium import spaces from bsk_rl.act.actions import Action, ActionBuilder @@ -16,6 +17,43 @@ logger = logging.getLogger(__name__) +def clean_action(action: Any, hybrid: bool = False) -> tuple[Any, Optional[float]]: + """Normalize an action to its selection and optional continuous parameter. + + Args: + action: A selection (integer index, target ID, or Target), or a + ``(selection, parameter)`` tuple. The parameter may be a scalar or + an array-like value with shape ``()`` or ``(1,)``. + hybrid: Whether to retain the continuous parameter. Otherwise, discard it. + + Returns: + The selection unchanged and the parameter as a Python float, or ``None`` + when absent, explicitly ``None``, or hybrid mode is disabled. The selected + action defines its meaning: a mode duration or desired capture offset in + seconds. + + Raises: + ValueError: If a tuple does not contain two items, or a retained parameter + has an unsupported shape, cannot be converted to a float, or is + negative or non-finite. + TypeError: If the parameter's type does not support conversion to a float. + """ + selection, value = action if isinstance(action, tuple) else (action, None) + + if not hybrid or value is None: + return selection, None + + value = np.asarray(value) + if value.shape not in ((), (1,)): + raise ValueError("Hybrid action requires a scalar or one-element parameter.") + + value = float(value.item()) + if not np.isfinite(value) or value < 0: + raise ValueError("Hybrid action parameter must be finite and nonnegative.") + + return selection, value + + class DiscreteActionBuilder(ActionBuilder): def __init__(self, satellite: "Satellite") -> None: """Processes actions for a discrete action space. @@ -25,16 +63,22 @@ def __init__(self, satellite: "Satellite") -> None: """ super().__init__(satellite) self.prev_action_key = None + self.hybrid = any([act.hybrid for act in self.action_spec]) def reset_post_sim_init(self) -> None: - """Log previous action key.""" + """Clear the previous action key after simulator initialization.""" super().reset_post_sim_init() self.prev_action_key = None @property - def action_space(self) -> spaces.Discrete: - """Discrete action space.""" - return spaces.Discrete(sum([act.n_actions for act in self.action_spec])) + def action_space(self) -> spaces.Discrete | spaces.Tuple: + """Return a discrete space, with a continuous parameter for hybrid actions.""" + n_actions = sum([act.n_actions for act in self.action_spec]) + if self.hybrid: + return spaces.Tuple( + (spaces.Discrete(n_actions), spaces.Box(low=0, high=np.inf, shape=(1,))) + ) + return spaces.Discrete(n_actions) @property def action_description(self) -> list[str]: @@ -47,21 +91,26 @@ def action_description(self) -> list[str]: actions.extend([f"{act.name}_{i}" for i in range(act.n_actions)]) return actions - def set_action(self, action: int) -> None: - """Sets the action based on the integer index. + def set_action(self, action: Union[int, tuple[int, float]]) -> None: + """Set an action by global index, with an optional continuous parameter. If the action is not an integer, the satellite will attempt to call ``set_action_override`` for each action, in order, until one works. + + Hybrid actions receive ``(local_index, parameter)``. Other actions receive + only their local index, even when the overall action space is hybrid. """ + action_index, parameter = clean_action(action, hybrid=self.hybrid) self.satellite.disable_timed_terminal_event() - if not np.issubdtype(type(action), np.integer): + if not np.issubdtype(type(action_index), np.integer): logger.warning( - f"Action '{action}' is not an integer. Will attempt to use compatible set_action_override method." + f"Action '{action_index}' is not an integer. Will attempt to use compatible set_action_override method." ) for act in self.action_spec: + payload = (action_index, parameter) if act.hybrid else action_index try: self.prev_action_key = act.set_action_override( - action, prev_action_key=self.prev_action_key + payload, prev_action_key=self.prev_action_key ) return except AttributeError: @@ -70,24 +119,27 @@ def set_action(self, action: int) -> None: pass else: raise ValueError( - f"Action '{action}' is not an integer and no compatible set_action_override method found." + f"Action '{action_index}' is not an integer and no compatible set_action_override method found." ) - index = 0 + offset = 0 for act in self.action_spec: - if index + act.n_actions > action: + if offset <= action_index < offset + act.n_actions: + local_index = action_index - offset + payload = (local_index, parameter) if act.hybrid else local_index self.prev_action_key = act.set_action( - action - index, prev_action_key=self.prev_action_key + payload, prev_action_key=self.prev_action_key ) return - index += act.n_actions - else: - raise ValueError(f"Action index {action} out of range.") + offset += act.n_actions + raise ValueError(f"Action index {action_index} out of range.") class DiscreteAction(Action): builder_type = DiscreteActionBuilder - def __init__(self, name: str = "discrete_act", n_actions: int = 1): + def __init__( + self, name: str = "discrete_act", n_actions: int = 1, hybrid: bool = False + ): """Base class for discrete, integer-indexable actions. A discrete action may represent multiple indexed actions of the same type. @@ -100,13 +152,18 @@ def __init__(self, name: str = "discrete_act", n_actions: int = 1): Args: name: Name of the action. n_actions: Number of actions available. + hybrid: If true, accept ``(selection, parameter)`` actions. The action + subclass defines the meaning of the continuous parameter. """ super().__init__(name=name) self.n_actions = n_actions + self.hybrid = hybrid @abstractmethod - def set_action(self, action: int, prev_action_key=None) -> str: - """Activate an action by local index.""" + def set_action( + self, action: Union[int, tuple[int, float]], prev_action_key=None + ) -> str: + """Activate an action by local index, optionally with a continuous parameter.""" pass @@ -117,6 +174,7 @@ def __init__( name=None, duration: Optional[float] = None, reset_task: bool = False, + hybrid: bool = False, ): """Discrete action to task a flight software action function. @@ -126,37 +184,50 @@ def __init__( Args: fsw_action: Name of the flight software function to task. name: Name of the action. If not specified, defaults to the ``fsw_action`` name. - duration: Duration of the action in seconds. Defaults to a large value so that + duration: Duration of the action in seconds, also used as a cap on a + hybrid action's requested duration. At least one simulator time step + is scheduled, even if this cap is smaller. Defaults to a large value so that the :class:`~bsk_rl.GeneralSatelliteTasking` ``max_step_duration`` controls step length. reset_task: If true, reset the action if the previous action was the same. Generally, this parameter should be false to ensure realistic, continuous operation of satellite modes; however, some Basilisk modules may require frequent resetting for normal operation. + hybrid: If true, the action duration can be set with the action. """ if name is None: name = fsw_action - super().__init__(name=name, n_actions=1) + super().__init__(name=name, n_actions=1, hybrid=hybrid) self.fsw_action = fsw_action self.reset_task = reset_task if duration is None: duration = 1e9 self.duration = duration - def set_action(self, action: int, prev_action_key=None) -> str: + def set_action( + self, action: Union[int, tuple[int, float]], prev_action_key=None + ) -> str: """Activate the ``fsw_action`` function. Args: - action: Should always be ``1``. + action: Local index ``0``, or ``(0, duration)`` in hybrid mode, where + duration is in seconds and is capped by the configured duration. prev_action_key: Previous action key. Returns: The name of the activated action. """ - assert action == 0 - self.satellite.logger.info(f"{self.name} tasked for {self.duration} seconds") + action_index, requested_duration = clean_action(action, hybrid=self.hybrid) + assert action_index == 0 + action_duration = ( + np.min([self.duration, requested_duration]) + if requested_duration is not None + else self.duration + ) + action_duration = max(self.simulator.sim_rate, action_duration) + self.satellite.logger.info(f"{self.name} tasked for {action_duration} seconds") self.satellite.update_timed_terminal_event( - self.simulator.sim_time + self.duration, info=f"for {self.fsw_action}" + self.simulator.sim_time + action_duration, info=f"for {self.fsw_action}" ) if self.reset_task or prev_action_key != self.fsw_action: @@ -166,7 +237,12 @@ def set_action(self, action: int, prev_action_key=None) -> str: class Charge(DiscreteFSWAction): - def __init__(self, name: Optional[str] = None, duration: Optional[float] = None): + def __init__( + self, + name: Optional[str] = None, + duration: Optional[float] = None, + hybrid: bool = False, + ): """Action to enter a sun-pointing charging mode (:class:`~bsk_rl.sim.fsw.BasicFSWModel.action_charge`). Charging will only occur if the satellite is in sunlight. @@ -174,34 +250,58 @@ def __init__(self, name: Optional[str] = None, duration: Optional[float] = None) Args: name: Action name. duration: Time to task action, in seconds. + hybrid: Whether to use a hybrid action space. """ - super().__init__(fsw_action="action_charge", name=name, duration=duration) + super().__init__( + fsw_action="action_charge", name=name, duration=duration, hybrid=hybrid + ) class Drift(DiscreteFSWAction): - def __init__(self, name: Optional[str] = None, duration: Optional[float] = None): + def __init__( + self, + name: Optional[str] = None, + duration: Optional[float] = None, + hybrid: bool = False, + ): """Action to disable all FSW tasks (:class:`~bsk_rl.sim.fsw.BaseFSWModel.action_drift`). Args: name: Action name. duration: Time to task action, in seconds. + hybrid: Whether to use a hybrid action space. """ - super().__init__(fsw_action="action_drift", name=name, duration=duration) + super().__init__( + fsw_action="action_drift", name=name, duration=duration, hybrid=hybrid + ) class NadirPoint(DiscreteFSWAction): - def __init__(self, name: Optional[str] = None, duration: Optional[float] = None): + def __init__( + self, + name: Optional[str] = None, + duration: Optional[float] = None, + hybrid: bool = False, + ): """Action to point nadir (:class:`~bsk_rl.sim.fsw.BaseFSWModel.action_nadir_point`). Args: name: Action name. duration: Time to task action, in seconds. + hybrid: Whether to use a hybrid action space. """ - super().__init__(fsw_action="action_nadir_point", name=name, duration=duration) + super().__init__( + fsw_action="action_nadir_point", name=name, duration=duration, hybrid=hybrid + ) class Desat(DiscreteFSWAction): - def __init__(self, name: Optional[str] = None, duration: Optional[float] = None): + def __init__( + self, + name: Optional[str] = None, + duration: Optional[float] = None, + hybrid: bool = False, + ): """Action to desaturate reaction wheels (:class:`~bsk_rl.sim.fsw.BasicFSWModel.action_desat`). This action must be called repeatedly to fully desaturate the reaction wheels. @@ -209,14 +309,24 @@ def __init__(self, name: Optional[str] = None, duration: Optional[float] = None) Args: name: Action name. duration: Time to task action, in seconds. + hybrid: Whether to use a hybrid action space. """ super().__init__( - fsw_action="action_desat", name=name, duration=duration, reset_task=True + fsw_action="action_desat", + name=name, + duration=duration, + reset_task=True, + hybrid=hybrid, ) class Downlink(DiscreteFSWAction): - def __init__(self, name: Optional[str] = None, duration: Optional[float] = None): + def __init__( + self, + name: Optional[str] = None, + duration: Optional[float] = None, + hybrid: bool = False, + ): """Action to transmit data from the data buffer (:class:`~bsk_rl.sim.fsw.ImagingFSWModel.action_downlink`). If not in range of a ground station (defined in @@ -226,19 +336,30 @@ def __init__(self, name: Optional[str] = None, duration: Optional[float] = None) Args: name: Action name. duration: Time to task action, in seconds. + hybrid: Whether to use a hybrid action space. """ - super().__init__(fsw_action="action_downlink", name=name, duration=duration) + super().__init__( + fsw_action="action_downlink", name=name, duration=duration, hybrid=hybrid + ) class Scan(DiscreteFSWAction): - def __init__(self, name: Optional[str] = None, duration: Optional[float] = None): + def __init__( + self, + name: Optional[str] = None, + duration: Optional[float] = None, + hybrid: bool = False, + ): """Action to collect data from a :class:`~bsk_rl.scene.UniformNadirScanning` (:class:`~bsk_rl.sim.fsw.ContinuousImagingFSWModel.action_nadir_scan`). Args: name: Action name. duration: Time to task action, in seconds. + hybrid: Whether to use a hybrid action space. """ - super().__init__(fsw_action="action_nadir_scan", name=name, duration=duration) + super().__init__( + fsw_action="action_nadir_scan", name=name, duration=duration, hybrid=hybrid + ) class Image(DiscreteAction): @@ -247,14 +368,15 @@ def __init__( n_ahead_image: int, max_duration: Optional[float] = None, name: str = "action_image", + hybrid: bool = False, ): """Actions to image upcoming target (:class:`~bsk_rl.sim.fsw.ImagingFSWModel.action_image`). Adds ``n_ahead_image`` actions to the action space, corresponding to the next ``n_ahead_image`` unimaged targets. The action may be unsuccessful if the target exits the satellite's field of regard before the satellite settles on the target - and takes an image. The action with stop as soon as the image is successfully - taken, or when the the target exits the field of regard. + and takes an image. The action will stop as soon as the image is successfully + taken, or when the target exits the field of regard. This action implements a ``set_action_override`` that allows a target to be tasked based on the target's ID string or the Target object. @@ -264,58 +386,147 @@ def __init__( max_duration: Maximum time to task the action, in seconds. If ``None``, tasks until the end of the next opportunity. name: Action name. + hybrid: Whether to accept ``(target_index, value)`` actions. With + either imaging time tolerance enabled, the value is a desired + capture-time offset from the current simulation time in seconds. + With both tolerances disabled, it is a maximum action duration + in seconds, capped by ``max_duration`` and with a minimum of one + simulator time step. It is not an exposure duration. The mode is + selected on each reset; tolerances must stay fixed during an episode. """ from bsk_rl.sats import ImagingSatellite self.satellite: "ImagingSatellite" self.max_duration = max_duration - super().__init__(name=name, n_actions=n_ahead_image) + super().__init__(name=name, n_actions=n_ahead_image, hybrid=hybrid) + + def reset_post_sim_init(self) -> None: + """Select the continuous parameter's meaning from the initialized controller.""" + super().reset_post_sim_init() + controller = getattr(self.satellite.fsw, "insControl", None) + lower = getattr(controller, "timeToleranceLower", 0) + upper = getattr(controller, "timeToleranceUpper", 0) + self._use_capture_time = lower > 0 or upper > 0 + self._time_tolerance_upper = upper * mc.NANO2SEC + if self.hybrid and self._use_capture_time: + self.satellite.logger.warning( + "Imaging time tolerances are enabled; the continuous Image action " + "value specifies a desired capture-time offset in seconds rather " + "than a maximum duration as for other hybrid actions." + ) def image( - self, target: Union[int, "Target", str], prev_action_key: Optional[str] = None + self, + target: Union[int, "Target", str], + prev_action_key: Optional[str] = None, + imaging_time: Optional[float] = None, + max_duration: Optional[float] = None, ) -> str: """Task or retask a satellite for imaging a target. Args: target: Target to image. prev_action_key: Previous action key. + imaging_time: Desired imaging time relative to the current simulation time, + in seconds. If ``None``, retain the controller's existing imaging time + and tolerances. Warn if either time tolerance is enabled. + max_duration: Per-action duration limit in seconds, capped by the + configured ``max_duration`` and with a minimum of one simulator + time step. If ``None``, use the configured limit. :meta private: """ target = self.satellite.parse_target_selection(target) + + if imaging_time is None and self._use_capture_time: + self.satellite.logger.warning( + "No imaging_time was provided while imaging time tolerances are enabled. " + "The controller's existing imagingTime and tolerances are retained.", + ) + if max_duration is None: + max_duration = self.max_duration + else: + if self.max_duration is not None: + max_duration = np.min([self.max_duration, max_duration]) + max_duration = max(self.simulator.sim_rate, max_duration) + if imaging_time is not None and self._time_tolerance_upper > 0.0: + deadline = imaging_time + self._time_tolerance_upper + max_duration = ( + np.min([max_duration, deadline]) + if max_duration is not None + else deadline + ) + max_duration = max(self.simulator.sim_rate, max_duration) + timing = [] + if imaging_time is not None: + timing.append( + f"requested capture at {self.simulator.sim_time + imaging_time:.1f}" + ) + if max_duration is not None: + timing.append(f"maximum duration {max_duration:.1f} seconds") + if timing: + self.satellite.logger.info(f"{target} imaging timing: " + "; ".join(timing)) if target.id != prev_action_key: + kwargs = {} + if imaging_time is not None: + kwargs["imaging_time"] = imaging_time self.satellite.task_target_for_imaging( - target, max_duration=self.max_duration + target, max_duration=max_duration, **kwargs ) else: - self.satellite.enable_target_window(target, max_duration=self.max_duration) + self.satellite.enable_target_window(target, max_duration=max_duration) + if imaging_time is not None: + self.satellite.update_imaging_time(imaging_time=imaging_time) return target.id - def set_action(self, action: int, prev_action_key: Optional[str] = None) -> str: + def set_action( + self, + action: Union[int, tuple[int, float]], + prev_action_key: Optional[str] = None, + ) -> str: """Image a target by local index. Args: - action: Index of the target to image. + action: Local target index, or ``(index, value)`` in hybrid mode. + The value is a desired capture-time offset in seconds when either + time tolerance is enabled, or a maximum action duration otherwise. prev_action_key: Previous action key. - :meta_private: + :meta private: """ - self.satellite.logger.info(f"target index {action} tasked") - return self.image(action, prev_action_key) + target_index, value = clean_action(action, hybrid=self.hybrid) + self.satellite.logger.info(f"target index {target_index} tasked") + kwargs = ( + {"imaging_time": value} + if self._use_capture_time + else {"max_duration": value} + ) + return self.image(target_index, prev_action_key, **kwargs) def set_action_override( - self, action: Union["Target", str], prev_action_key: Optional[str] = None + self, + action: Union[Union["Target", str], tuple[Union["Target", str], float]], + prev_action_key: Optional[str] = None, ) -> str: """Image a target by target index, Target, or ID. Args: - action: Target to image in the form of a Target object, target ID, or target index. + action: Target object, target ID, or target index. In hybrid mode, + also accepts ``(target, value)``. The value is a desired capture-time + offset in seconds when either time tolerance is enabled, or a + maximum action duration otherwise. prev_action_key: Previous action key. - :meta_private: + :meta private: """ - return self.image(action, prev_action_key) + target, value = clean_action(action, hybrid=self.hybrid) + kwargs = ( + {"imaging_time": value} + if self._use_capture_time + else {"max_duration": value} + ) + return self.image(target, prev_action_key, **kwargs) class Broadcast(DiscreteAction): @@ -323,6 +534,7 @@ def __init__( self, name: str = "action_broadcast", duration: Optional[float] = None, + hybrid: bool = False, ): """Action to broadcast data to all satellites in the simulation. @@ -331,23 +543,39 @@ def __init__( Args: name: Action name. - duration: [s] Time to idle before communicating. + duration: [s] Time to idle before communicating, also used as a cap + on a hybrid action's requested duration. At least one simulator + time step is scheduled, even if this cap is smaller. + hybrid: Whether to use the hybrid action format. """ - super().__init__(name=name, n_actions=1) + super().__init__(name=name, n_actions=1, hybrid=hybrid) if duration is None: duration = 1e9 self.duration = duration def reset_post_sim_init(self) -> None: - """Log previous action key.""" + """Clear pending broadcasts after simulator initialization.""" super().reset_post_sim_init() self.broadcast_pending = False - def set_action(self, action: int, prev_action_key=None) -> str: - assert action == 0 + def set_action( + self, action: Union[int, tuple[int, float]], prev_action_key=None + ) -> str: + """Schedule a broadcast using local index ``0`` or ``(0, duration_seconds)``.""" + action_index, requested_duration = clean_action(action, hybrid=self.hybrid) + assert action_index == 0 self.broadcast_pending = True + action_duration = ( + np.min([self.duration, requested_duration]) + if requested_duration is not None + else self.duration + ) + action_duration = max(self.simulator.sim_rate, action_duration) + self.satellite.logger.info( + f"{self.name} tasked for {action_duration:.1f} seconds" + ) self.satellite.update_timed_terminal_event( - self.simulator.sim_time + self.duration, info=f"for broadcast" + self.simulator.sim_time + action_duration, info="for broadcast" ) return self.name diff --git a/src/bsk_rl/act/rso_imaging.py b/src/bsk_rl/act/rso_imaging.py index 0fda23f9..9e4bfcc5 100644 --- a/src/bsk_rl/act/rso_imaging.py +++ b/src/bsk_rl/act/rso_imaging.py @@ -5,7 +5,7 @@ import numpy as np from Basilisk.utilities import macros -from bsk_rl.act.discrete_actions import DiscreteAction +from bsk_rl.act.discrete_actions import DiscreteAction, clean_action from bsk_rl.utils.rso_imaging import candidate_snapshot @@ -95,6 +95,7 @@ def _task(self, target) -> str: def set_action(self, action: int, prev_action_key=None) -> str: """Decode the same ordered snapshot used to construct observations.""" + action, _ = clean_action(action) if not 0 <= action < self.n_actions: raise ValueError("RSO imaging action index out of range.") snapshot = getattr(self.satellite, "_rso_candidate_snapshot", None) @@ -126,6 +127,7 @@ def set_action(self, action: int, prev_action_key=None) -> str: def set_action_override(self, action, prev_action_key=None) -> str: """Task an explicitly identified target without parsing spacecraft names.""" + action, _ = clean_action(action) target_id = action if isinstance(action, str) else getattr(action, "id", None) if target_id is None: raise TypeError("Expected an RSO target ID or RSOTarget definition.") diff --git a/src/bsk_rl/gym.py b/src/bsk_rl/gym.py index a21d867c..f4dc0bf1 100644 --- a/src/bsk_rl/gym.py +++ b/src/bsk_rl/gym.py @@ -41,14 +41,32 @@ def is_no_action(action): if isinstance(action, (float, np.floating)): # float32 cannot represent 2**31-1 exactly return bool(np.isclose(action, NO_ACTION, rtol=0.0, atol=1.0)) - if isinstance(action, (np.ndarray, list, tuple)): - arr = np.asarray(action) - if arr.size == 0: + if isinstance(action, (list, tuple)): + # Quickly reject active actions, including ordinary hybrid tuples. + if not action or not is_no_action(action[0]): return False - if np.issubdtype(arr.dtype, np.integer): - return bool(np.all(arr == NO_ACTION)) - return bool(np.all(np.isclose(arr, NO_ACTION, rtol=0.0, atol=1.0))) - return False + + first_type = type(action[0]) + numeric = isinstance(action[0], (int, float, np.integer, np.floating)) + homogeneous = all(type(value) is first_type for value in action) + if not numeric or not homogeneous: + # The first element has already been checked. Preserve exact integer + # comparisons instead of promoting mixed integer/float inputs. + return all(is_no_action(value) for value in action[1:]) + + arr = np.asarray(action) + elif isinstance(action, np.ndarray): + arr = action + else: + return False + + if arr.size == 0: + return False + if arr.dtype == object: + return all(is_no_action(value) for value in arr.flat) + if np.issubdtype(arr.dtype, np.integer): + return bool(np.all(arr == NO_ACTION)) + return bool(np.all(np.isclose(arr, NO_ACTION, rtol=0.0, atol=1.0))) def no_action_like(action): @@ -785,6 +803,10 @@ def action_spaces(self) -> dict[AgentID, spaces.Space[SatAct]]: else satellites[0].action_space.dtype ), ) + elif isinstance(satellites[0].action_space, spaces.Tuple): + act_spaces[agent] = spaces.Tuple( + tuple(sat.action_space for sat in satellites) + ) return act_spaces @functools.lru_cache(maxsize=None) @@ -911,29 +933,46 @@ def _decompose_meta_action( self, agent: AgentID, action: SatAct ) -> dict[Satellite, SatAct]: """Decompose a meta agent action into satellite actions.""" - sat_to_action_map = {} - i = 0 - for satellite in self.meta_agent_groupings[agent]: - action_len = satellite.action_space.shape - if len(action_len) == 0: - action_len = 1 + satellites = self.meta_agent_groupings[agent] + if isinstance(satellites[0].action_space, spaces.Tuple): + if is_no_action(action): + sat_to_action_map = dict.fromkeys(satellites) + elif len(satellites) == 1: + sat_to_action_map = {satellites[0]: action} else: - action_len = action_len[0] - - if isinstance(action, (list, tuple, np.ndarray)): if ( - not self.only_retask_idle_meta_agent_members - or satellite.requires_retasking + not isinstance(action, (list, tuple, np.ndarray)) + or (isinstance(action, np.ndarray) and action.ndim == 0) + or len(action) != len(satellites) ): + raise ValueError( + "Expected one hybrid action per satellite " + f"in meta agent '{agent}' ({len(satellites)} actions)." + ) + sat_to_action_map = dict(zip(satellites, action)) + else: + sat_to_action_map = {} + i = 0 + for satellite in satellites: + action_len = satellite.action_space.shape + if len(action_len) == 0: + action_len = 1 + else: + action_len = action_len[0] + + if isinstance(action, (list, tuple, np.ndarray)): if action_len == 1: sat_to_action_map[satellite] = action[i] else: sat_to_action_map[satellite] = action[i : i + action_len] + i += action_len else: + sat_to_action_map[satellite] = action + + if self.only_retask_idle_meta_agent_members: + for satellite in satellites: + if not satellite.requires_retasking: sat_to_action_map[satellite] = None - i += action_len - else: - sat_to_action_map[satellite] = action return sat_to_action_map diff --git a/src/bsk_rl/sats/access_satellite.py b/src/bsk_rl/sats/access_satellite.py index e4972d9f..95706dc3 100644 --- a/src/bsk_rl/sats/access_satellite.py +++ b/src/bsk_rl/sats/access_satellite.py @@ -623,10 +623,10 @@ def side_effect(sim): self._image_event_name, macros.sec2nano(self.fsw.fsw_rate), True, - conditionFunction=lambda sim: self.dynamics.storageUnit.storageUnitDataOutMsg.read().storedData[ - 0 - ] - > current_data_level, + conditionFunction=lambda sim: ( + self.dynamics.storageUnit.storageUnitDataOutMsg.read().storedData[0] + > current_data_level + ), actionFunction=side_effect, terminal=self.variable_interval, ) @@ -700,7 +700,10 @@ def side_effect(sim): ) def task_target_for_imaging( - self, target: "Target", max_duration: Optional[float] = None + self, + target: "Target", + max_duration: Optional[float] = None, + imaging_time: float | None = None, ): """Task the satellite to image a target. @@ -708,14 +711,37 @@ def task_target_for_imaging( target: Selected target max_duration: [s] Maximum duration to wait for imaging. If None, wait until the end of the target's access window. + imaging_time: [s] Desired capture offset from the current simulation + time, not an exposure duration. If None, retain the controller's + existing imaging time and tolerances. """ msg = f"{target} tasked for imaging" self.logger.info(msg) - self.fsw.action_image(target.r_LP_P, self.buffer_name) + kwargs = {} + if imaging_time is not None: + kwargs["imaging_time"] = imaging_time + self.fsw.action_image(target.r_LP_P, self.buffer_name, **kwargs) self.enable_target_window(target, max_duration=max_duration) self.draw_imaging_line(target) self.latest_target = target + def update_imaging_time(self, imaging_time: float) -> None: + """Update the imaging time for the current target. + + Args: + imaging_time: [s] Desired capture offset from the current simulation + time. This is converted to an absolute controller time. + """ + try: + self.fsw.insControl.imagingTime = macros.sec2nano( + imaging_time + self.simulator.sim_time + ) + except Exception as e: + logger.debug( + "insControl.imagingTime not available; skipping update (reason: %s)", + e, + ) + @vizard.visualize def draw_imaging_line( self, target: "Target", vizSupport=None, vizInstance=None diff --git a/src/bsk_rl/sim/fsw/ground_imaging.py b/src/bsk_rl/sim/fsw/ground_imaging.py index 4423510b..5506b5fe 100644 --- a/src/bsk_rl/sim/fsw/ground_imaging.py +++ b/src/bsk_rl/sim/fsw/ground_imaging.py @@ -1,5 +1,6 @@ """FSW models for ground imaging scenarios.""" +import logging from typing import TYPE_CHECKING, Iterable import numpy as np @@ -9,6 +10,7 @@ scanningInstrumentController, simpleInstrumentController, ) +from Basilisk.utilities import macros as mc from bsk_rl.sim import dyn from bsk_rl.sim.fsw import BasicFSWModel, SteeringFSWModel, Task, action @@ -19,6 +21,8 @@ if TYPE_CHECKING: # pragma: no cover from bsk_rl.sim.dyn import DynamicsModelABC +logger = logging.getLogger(__name__) + class ImagingFSWModel(BasicFSWModel): """Extend FSW with instrument pointing and triggering control.""" @@ -76,6 +80,7 @@ def _create_module_data(self) -> None: def _setup_fsw_objects(self, **kwargs) -> None: self.setup_location_pointing(**kwargs) self.setup_instrument_controller(**kwargs) + self.setup_instrument_time_tolerance(**kwargs) self.show_sensor() @default_args(inst_pHat_B=[0, 0, 1]) @@ -140,6 +145,50 @@ def setup_instrument_controller( self._add_model_to_task(self.insControl, priority=987) + @default_args(timeToleranceLower=0, timeToleranceUpper=0) + def setup_instrument_time_tolerance( + self, + timeToleranceLower: float | int = 0, + timeToleranceUpper: float | int = 0, + **kwargs, + ): + """Set the instrument controller time tolerance. + + The instrument controller will only trigger an image if the current time is within + the time tolerance window. This is useful for scheduling imaging at a specific time. + + Args: + timeToleranceLower: [s] Allowed time before the desired imaging time. + Zero disables the lower time bound. + timeToleranceUpper: [s] Allowed time after the desired imaging time. + Zero disables the upper time bound. + kwargs: Passed to other setup functions. + + Raises: + ValueError: If either tolerance is negative, or non-finite. + """ + tolerances_ns = {} + for name, value in ( + ("timeToleranceLower", timeToleranceLower), + ("timeToleranceUpper", timeToleranceUpper), + ): + bound = "lower" if name == "timeToleranceLower" else "upper" + if not np.isfinite(value) or value < 0: + raise ValueError( + f"{name} must be finite and nonnegative; " + f"0 disables the {bound} time bound." + ) + tolerances_ns[name] = mc.sec2nano(value) + + try: + self.insControl.timeToleranceLower = tolerances_ns["timeToleranceLower"] + self.insControl.timeToleranceUpper = tolerances_ns["timeToleranceUpper"] + except Exception as e: + logger.warning( + "insControl.timeTolerance not available; continuing without it (reason: %s)", + e, + ) + @vizard.visualize def show_sensor(self, vizInterface=None, vizSupport=None): """Visualize the sensor in Vizard.""" @@ -163,7 +212,9 @@ def reset_for_action(self) -> None: return super().reset_for_action() @action - def action_image(self, r_LP_P: Iterable[float], data_name: str) -> None: + def action_image( + self, r_LP_P: Iterable[float], data_name: str, imaging_time: float | None = None + ) -> None: """Attempt to image a target at a location. This action sets the target attitude to one tracking a ground location. If the @@ -174,8 +225,21 @@ def action_image(self, r_LP_P: Iterable[float], data_name: str) -> None: Args: r_LP_P: [m] Planet-fixed planet relative target location. data_name: Data buffer to store image data to. + imaging_time: [s] Desired capture offset from the current simulation + time, not an exposure duration. If None, retain the controller's + existing imaging time and tolerances. """ self.insControl.controllerStatus = 1 + if imaging_time is not None: + try: + self.insControl.imagingTime = mc.sec2nano( + imaging_time + self.simulator.sim_time + ) + except Exception as e: + logger.warning( + "insControl.imagingTime not available; proceeding without setting it (reason: %s)", + e, + ) self.dynamics.instrumentPowerSink.powerStatus = 1 self.dynamics.imagingTarget.r_LP_P_Init = r_LP_P self.dynamics.instrument.nodeDataName = data_name From abf5a850ef7354c884be6cbab6cac20c32547a99 Mon Sep 17 00:00:00 2001 From: LorenzzoQM Date: Fri, 23 Oct 2026 16:31:57 -0600 Subject: [PATCH 2/4] [#363] Add test coverage for hybrid actions --- .../act/test_int_actions_discrete.py | 71 ++- .../act/test_int_imaging_timing.py | 276 ++++++++++++ .../sim/test_int_instrument_time_windows.py | 199 +++++++++ tests/integration/test_int_gym_env.py | 123 +++++- tests/unittest/act/test_actions.py | 415 ++++++++++++++++-- tests/unittest/act/test_hybrid_actions.py | 248 +++++++++++ .../unittest/act/test_rso_imaging_actions.py | 35 ++ tests/unittest/sats/test_access_satellite.py | 32 +- tests/unittest/sim/test_fsw.py | 53 ++- tests/unittest/test_gym_env.py | 160 +++++++ 10 files changed, 1562 insertions(+), 50 deletions(-) create mode 100644 tests/integration/act/test_int_imaging_timing.py create mode 100644 tests/integration/sim/test_int_instrument_time_windows.py create mode 100644 tests/unittest/act/test_hybrid_actions.py diff --git a/tests/integration/act/test_int_actions_discrete.py b/tests/integration/act/test_int_actions_discrete.py index a79d5fa0..11479cac 100644 --- a/tests/integration/act/test_int_actions_discrete.py +++ b/tests/integration/act/test_int_actions_discrete.py @@ -66,7 +66,6 @@ def test_downlink(self): assert self.env.unwrapped.satellite.dynamics.storage_level < storage_init def test_image_by_name(self): - # Smoketest self.env.reset() target = self.env.unwrapped.satellite.find_next_opportunities( n=10, types="target" @@ -76,7 +75,75 @@ def test_image_by_name(self): n=10, types="target" )[9]["object"] self.env.step(target.id) - assert True + + +class TestHybridImagingAndDownlink: + class ImageSat(sats.ImagingSatellite): + dyn_type = dyn.GroundStationDynModel + fsw_type = fsw.ImagingFSWModel + observation_spec: ClassVar[list[obs.Observation]] = [obs.Time()] + action_spec: ClassVar[list[act.Action]] = [ + act.Downlink(hybrid=True), + act.Image(n_ahead_image=10, hybrid=True), + ] + + env = gym.make( + "SatelliteTasking-v1", + satellite=ImageSat( + "EO_1", + sat_args=ImageSat.default_sat_args( + oe=random_orbit, + imageAttErrorRequirement=0.05, + imageRateErrorRequirement=0.05, + instrumentBaudRate=1.0, + dataStorageCapacity=3.0, + transmitterBaudRate=-1.0, + ), + ), + scenario=UniformTargets(n_targets=1000), + rewarder=data.UniqueImageReward(), + sim_rate=1.0, + time_limit=10000.0, + max_step_duration=1e9, + disable_env_checker=True, + ) + + def test_image(self): + self.env.reset() + storage_init = self.env.unwrapped.satellite.dynamics.storage_level + for i in range(10): + self.env.step((i + 1, 1000.0)) + assert self.env.unwrapped.satellite.dynamics.storage_level > storage_init + assert self.env.unwrapped.satellite.dynamics.storage_level == approx(3.0) + + @pytest.mark.flaky(retries=5, delay=1) + def test_downlink(self): + self.env.reset() + for i in range(1, 10): + self.env.step((0, 5.0 * i)) + assert self.env.unwrapped.simulator.sim_time == approx( + np.sum([5 * i for i in range(1, 10)]), abs=1e-3 + ) + + @pytest.mark.parametrize("duration", [0.0, 0.25]) + def test_downlink_minimum_duration(self, duration): + self.env.reset(seed=0) + for step in range(1, 3): + self.env.step((0, duration)) + assert self.env.unwrapped.simulator.sim_time == approx( + step * self.env.unwrapped.simulator.sim_rate + ) + + def test_image_by_name(self): + self.env.reset() + target = self.env.unwrapped.satellite.find_next_opportunities( + n=10, types="target" + )[9]["object"] + self.env.step(target) + target = self.env.unwrapped.satellite.find_next_opportunities( + n=10, types="target" + )[9]["object"] + self.env.step(target.id) ########################### diff --git a/tests/integration/act/test_int_imaging_timing.py b/tests/integration/act/test_int_imaging_timing.py new file mode 100644 index 00000000..6afe8f5c --- /dev/null +++ b/tests/integration/act/test_int_imaging_timing.py @@ -0,0 +1,276 @@ +"""Hybrid imaging times must control physical capture and storage in an environment.""" + +import logging +from types import SimpleNamespace +from typing import ClassVar + +import numpy as np +import pytest +from Basilisk.architecture import messaging +from Basilisk.utilities import macros + +from bsk_rl import SatelliteTasking, act, data, obs, sats +from bsk_rl.scene import UniformTargets +from bsk_rl.sim import dyn, fsw + + +class TimedCamera(sats.ImagingSatellite): + dyn_type = dyn.GroundStationDynModel + fsw_type = fsw.ImagingFSWModel + observation_spec: ClassVar[list[obs.Observation]] = [obs.Time()] + action_spec: ClassVar[list[act.Action]] = [ + act.Image(1, hybrid=True), + act.Drift(hybrid=True), + ] + + +@pytest.fixture +def make_timed_imaging(): + environments = [] + + def make(lower=2.0, upper=3.0, sim_rate=1.0, ready_at=0.0, max_duration=None): + env = SatelliteTasking( + satellite=TimedCamera( + "timed_camera", + sat_args=dict( + timeToleranceLower=lower, + timeToleranceUpper=upper, + instrumentBaudRate=1.0, + dataStorageCapacity=10.0, + ), + ), + scenario=UniformTargets(1), + rewarder=data.UniqueImageReward(), + sim_rate=sim_rate, + max_step_duration=sim_rate, + time_limit=40.0, + log_level="ERROR", + ) + environments.append(env) + env.satellite.action_builder.action_spec[0].max_duration = max_duration + env.reset(seed=0) + satellite = env.satellite + target = env.scenario.targets[0] + # Control visibility and guidance to isolate timing from orbital access + # and attitude settling. The instrument and storage remain real models. + satellite.opportunities = [ + dict(object=target, window=(0.0, 30.0), type="target") + ] + guidance = messaging.AttGuidMsg() + access_payload = messaging.AccessMsgPayload() + access_payload.hasAccess = 1 + access = messaging.AccessMsg().write(access_payload) + controller = satellite.fsw.insControl + if not all( + hasattr(type(controller), name) + for name in ("imagingTime", "timeToleranceLower", "timeToleranceUpper") + ): + pytest.skip("This Basilisk build does not support imaging-time tolerances.") + controller.attGuidInMsg.subscribeTo(guidance) + controller.locationAccessInMsg.subscribeTo(access) + commands = controller.deviceCmdOutMsg.recorder() + env.simulator.AddModelToTask( + satellite.fsw.LocPointTask.name + satellite.name, commands, ModelPriority=0 + ) + + def step(action): + payload = messaging.AttGuidMsgPayload() + if env.simulator.sim_time + sim_rate < ready_at: + payload.sigma_BR = [1.0, 0, 0] + guidance.write(payload) + env.step(action) + + return SimpleNamespace( + env=env, + satellite=satellite, + target=target, + controller=controller, + commands=commands, + step=step, + ) + + yield make + + for env in environments: + env.close() + + +@pytest.mark.parametrize( + "lower,upper,ready_at,expected_capture,sim_rate", + [ + (0.0, 0.0, 6.0, 6.0, 1.0), + (0.0, 0.0, 20.0, None, 1.0), + (2.0, 0.0, 6.0, 10.0, 1.0), + (2.0, 0.0, 20.0, 20.0, 1.0), + (0.0, 3.0, 6.0, 6.0, 1.0), + (0.0, 3.0, 20.0, None, 1.0), + (2.0, 3.0, 6.0, 10.0, 1.0), + (2.0, 3.0, 20.0, None, 1.0), + (2.0, 3.0, 6.0, 10.0, 0.5), + (2.0, 3.0, 20.0, None, 0.5), + ], + ids=[ + "duration_captures_before_deadline", + "duration_expires_before_ready", + "lower_waits", + "lower_allows_late", + "upper_allows_early", + "upper_blocks_late", + "both_wait", + "both_block_late", + "half_step_wait", + "half_step_block_late", + ], +) +def test_hybrid_time_windows_control_physical_image_storage( + make_timed_imaging, + lower, + upper, + ready_at, + expected_capture, + sim_rate, +): + imaging = make_timed_imaging(lower, upper, sim_rate, ready_at) + imaging.step((1, np.array([4.0], dtype=np.float32))) + while imaging.env.simulator.sim_time < 4.0: + imaging.step(None) + assert imaging.satellite.dynamics.storage_level == 0.0 + + # At t=4, eight seconds means a duration ending at t=12 when bounds are + # disabled, or a desired capture at t=12 with enabled bounds at t=10/t=15. + imaging.step((0, np.array([8.0], dtype=np.float32))) + assert imaging.controller.imagingTime == ( + macros.sec2nano(12.0) if lower or upper else 0 + ) + assert imaging.controller.timeToleranceLower == macros.sec2nano(lower) + assert imaging.controller.timeToleranceUpper == macros.sec2nano(upper) + while ( + imaging.env.simulator.sim_time < 23.0 + and not imaging.satellite.requires_retasking + ): + imaging.step(None) + + commands = imaging.commands + capture_times = commands.times()[commands.deviceCmd == 1] * macros.NANO2SEC + if expected_capture is None: + assert capture_times.size == 0 + assert imaging.satellite.dynamics.storage_level == 0.0 + assert not imaging.satellite.data_store.data.imaged + if lower == upper == 0: + assert imaging.env.simulator.sim_time == pytest.approx(12.0) + else: + np.testing.assert_allclose(capture_times, [expected_capture], rtol=0, atol=1e-9) + assert imaging.satellite.dynamics.storage_level == pytest.approx(1.0) + assert imaging.satellite.data_store.data.imaged == {imaging.target} + + +def test_retask_same_target_changes_physical_capture_time(make_timed_imaging): + imaging = make_timed_imaging() + imaging.step((imaging.target.id, np.array([20.0]))) + assert imaging.env.simulator.sim_time == 1.0 + + imaging.step((imaging.target.id, np.array([8.0]))) + + assert imaging.controller.imagingTime == macros.sec2nano(9) + assert imaging.controller.timeToleranceLower == macros.sec2nano(2) + assert imaging.controller.timeToleranceUpper == macros.sec2nano(3) + while imaging.env.simulator.sim_time < 10.0: + imaging.step(None) + commands = imaging.commands + np.testing.assert_array_equal( + commands.times()[commands.deviceCmd == 1], [macros.sec2nano(7)] + ) + assert imaging.satellite.dynamics.storage_level == pytest.approx(1.0) + + +def test_untimed_retask_warns_and_keeps_physical_schedule(make_timed_imaging, caplog): + imaging = make_timed_imaging() + imaging.step((imaging.target.id, np.array([20.0]))) + + with caplog.at_level(logging.WARNING, logger=imaging.satellite.logger.name): + imaging.step(imaging.target.id) + + assert "existing imagingTime and tolerances are retained" in caplog.text + assert imaging.controller.imagingTime == macros.sec2nano(20) + assert imaging.controller.timeToleranceLower == macros.sec2nano(2) + assert imaging.controller.timeToleranceUpper == macros.sec2nano(3) + while imaging.env.simulator.sim_time < 21.0: + imaging.step(None) + commands = imaging.commands + np.testing.assert_array_equal( + commands.times()[commands.deviceCmd == 1], [macros.sec2nano(18)] + ) + assert imaging.satellite.dynamics.storage_level == pytest.approx(1.0) + + +@pytest.mark.parametrize( + "upper,imaging_time,cap,expected_end", + [ + (3.0, 8.0, None, 15.0), + (3.0, 8.0, 5.0, 9.0), + (0.0, 8.0, 5.0, 9.0), + (0.0, 8.0, None, 30.0), + (0.25, 0.0, None, 5.0), + (3.0, 8.0, 0.25, 5.0), + ], + ids=[ + "upper_deadline", + "duration_cap", + "cap_without_upper", + "access_end", + "minimum_step", + "cap_below_step", + ], +) +def test_imaging_deadline_limits_real_step( + make_timed_imaging, upper, imaging_time, cap, expected_end +): + # An invalid pointing input prevents an early successful-image event. + imaging = make_timed_imaging(upper=upper, ready_at=100, max_duration=cap) + imaging.step((1, 4.0)) + while imaging.env.simulator.sim_time < 4.0: + imaging.step(None) + imaging.env.simulator.max_step_duration = 100.0 + + imaging.step((0, np.array([imaging_time]))) + + assert imaging.env.simulator.sim_time == pytest.approx(expected_end) + assert imaging.satellite.requires_retasking + assert imaging.satellite.dynamics.storage_level == 0.0 + + +@pytest.mark.parametrize( + "duration,cap,expected_end", + [(0.0, None, 1.0), (0.25, None, 1.0), (5.0, None, 5.0), (20.0, 10.0, 10.0)], +) +def test_duration_mode_limits_real_step_without_setting_capture_time( + make_timed_imaging, + duration, + cap, + expected_end, +): + imaging = make_timed_imaging(lower=0, upper=0, ready_at=100, max_duration=cap) + imaging.controller.imagingTime = macros.sec2nano(200) + imaging.env.simulator.max_step_duration = 100.0 + + imaging.step((imaging.target.id, np.array([duration], dtype=np.float32))) + + assert imaging.env.simulator.sim_time == pytest.approx(expected_end) + assert imaging.controller.imagingTime == macros.sec2nano(200) + assert imaging.satellite.requires_retasking + assert imaging.satellite.dynamics.storage_level == 0.0 + + +def test_reset_selects_mode_from_realized_tolerances(make_timed_imaging): + lower_values = iter((0.0, 2.0, 0.0)) + imaging = make_timed_imaging(lower=lambda: next(lower_values), upper=0) + action = imaging.satellite.action_builder.action_spec[0] + assert not action._use_capture_time + + imaging.env.reset(seed=1) + assert imaging.satellite.fsw.insControl.timeToleranceLower == macros.sec2nano(2) + assert action._use_capture_time + + imaging.env.reset(seed=2) + assert imaging.satellite.fsw.insControl.timeToleranceLower == 0 + assert not action._use_capture_time diff --git a/tests/integration/sim/test_int_instrument_time_windows.py b/tests/integration/sim/test_int_instrument_time_windows.py new file mode 100644 index 00000000..d8a8d074 --- /dev/null +++ b/tests/integration/sim/test_int_instrument_time_windows.py @@ -0,0 +1,199 @@ +"""Exercise BSK-RL timing configuration against the real Basilisk controller. + +Guidance and access messages are controlled so that each test isolates the time +window. No pointing, orbit, or visibility randomness can hide a timing failure. +""" + +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest +from Basilisk.architecture import messaging +from Basilisk.fswAlgorithms import simpleInstrumentController +from Basilisk.utilities import macros + +from bsk_rl.sats import ImagingSatellite +from bsk_rl.sim.fsw import ImagingFSWModel + + +@pytest.fixture +def imaging(): + controller = simpleInstrumentController.simpleInstrumentController() + if not all( + hasattr(type(controller), name) + for name in ("imagingTime", "timeToleranceLower", "timeToleranceUpper") + ): + pytest.skip("This Basilisk build does not support imaging-time tolerances.") + guidance = messaging.AttGuidMsg().write(messaging.AttGuidMsgPayload()) + access_payload = messaging.AccessMsgPayload() + access_payload.hasAccess = 1 + access = messaging.AccessMsg().write(access_payload) + controller.attGuidInMsg.subscribeTo(guidance) + controller.locationAccessInMsg.subscribeTo(access) + controller.attErrTolerance = 0.01 + controller.useRateTolerance = 1 + controller.rateErrTolerance = 0.01 + # Initialize the native output message before calling the model directly. + controller.SelfInit() + controller.Reset(0) + + fsw = object.__new__(ImagingFSWModel) + fsw.logger = Mock() + fsw.satellite = SimpleNamespace( + name="timed_camera", + simulator=SimpleNamespace(sim_time=100.0, enableTask=Mock()), + dynamics=SimpleNamespace( + reset_for_action=Mock(), + instrumentPowerSink=SimpleNamespace(powerStatus=0), + imagingTarget=SimpleNamespace(r_LP_P_Init=None), + instrument=SimpleNamespace(nodeDataName=None), + ), + ) + fsw.insControl = controller + fsw.tasks = [] + fsw.fsw_proc = Mock() + fsw._zero_gateway_msgs = Mock() + return SimpleNamespace( + fsw=fsw, + controller=controller, + guidance=guidance, + access=access, + ) + + +def configure_window(imaging, lower, upper): + """Set tolerances through the same BSK-RL setup method used at reset.""" + ImagingFSWModel.LocPointTask.setup_instrument_time_tolerance( + SimpleNamespace(insControl=imaging.controller), + timeToleranceLower=lower, + timeToleranceUpper=upper, + ) + + +@pytest.mark.parametrize( + "lower,upper", + [(0.0, 0.0), (2.0, 0.0), (0.0, 3.0), (2.0, 3.0)], + ids=["disabled", "lower_only", "upper_only", "both"], +) +@pytest.mark.parametrize( + "time_ns", + [ + macros.sec2nano(108) - 1, + macros.sec2nano(108), + macros.sec2nano(113), + macros.sec2nano(113) + 1, + ], + ids=[ + "before_lower", + "at_lower", + "at_upper", + "after_upper", + ], +) +def test_capture_command_respects_enabled_bounds(imaging, lower, upper, time_ns): + configure_window(imaging, lower, upper) + imaging.fsw.action_image([6378137, 0, 0], "images", imaging_time=10.0) + assert imaging.controller.imagingTime == macros.sec2nano(110) + + imaging.controller.UpdateState(time_ns) + + expected = (lower == 0 or time_ns >= macros.sec2nano(108)) and ( + upper == 0 or time_ns <= macros.sec2nano(113) + ) + assert imaging.controller.deviceCmdOutMsg.read().deviceCmd == int(expected) + assert imaging.controller.imaged == int(expected) + + +@pytest.mark.parametrize("imaging_time", [0.0, 2.5]) +def test_capture_time_is_relative_to_nonzero_simulation_time(imaging, imaging_time): + configure_window(imaging, 0.5, 0.75) + + imaging.fsw.action_image([6378137, 0, 0], "images", imaging_time=imaging_time) + + assert imaging.controller.imagingTime == macros.sec2nano(100 + imaging_time) + assert imaging.controller.timeToleranceLower == macros.sec2nano(0.5) + assert imaging.controller.timeToleranceUpper == macros.sec2nano(0.75) + assert imaging.fsw.dynamics.instrumentPowerSink.powerStatus == 1 + assert imaging.fsw.dynamics.instrument.nodeDataName == "images" + + +def test_omitted_capture_time_preserves_existing_schedule(imaging): + configure_window(imaging, 2.0, 3.0) + imaging.controller.imagingTime = macros.sec2nano(110) + + imaging.fsw.action_image([6378137, 0, 0], "images") + + assert imaging.controller.imagingTime == macros.sec2nano(110) + assert imaging.controller.timeToleranceLower == macros.sec2nano(2) + assert imaging.controller.timeToleranceUpper == macros.sec2nano(3) + imaging.controller.UpdateState(macros.sec2nano(107)) + assert imaging.controller.deviceCmdOutMsg.read().deviceCmd == 0 + imaging.controller.UpdateState(macros.sec2nano(108)) + assert imaging.controller.deviceCmdOutMsg.read().deviceCmd == 1 + + +@pytest.mark.parametrize("imaging_time", [0.0, 2.5]) +def test_retask_updates_capture_time_without_changing_tolerances(imaging, imaging_time): + configure_window(imaging, 0.5, 0.75) + imaging.fsw.action_image([6378137, 0, 0], "images", imaging_time=10.0) + imaging.fsw.simulator.sim_time = 120.0 + satellite = SimpleNamespace(fsw=imaging.fsw, simulator=imaging.fsw.simulator) + + ImagingSatellite.update_imaging_time(satellite, imaging_time) + + desired_ns = macros.sec2nano(120 + imaging_time) + assert imaging.controller.imagingTime == desired_ns + assert imaging.controller.timeToleranceLower == macros.sec2nano(0.5) + assert imaging.controller.timeToleranceUpper == macros.sec2nano(0.75) + imaging.controller.UpdateState(desired_ns - macros.sec2nano(0.5) - 1) + assert imaging.controller.deviceCmdOutMsg.read().deviceCmd == 0 + imaging.controller.UpdateState(desired_ns) + assert imaging.controller.deviceCmdOutMsg.read().deviceCmd == 1 + + +def test_disabling_bounds_removes_previous_window(imaging): + configure_window(imaging, 2.0, 3.0) + imaging.fsw.action_image([6378137, 0, 0], "images", imaging_time=10.0) + imaging.controller.UpdateState(macros.sec2nano(114)) + assert imaging.controller.deviceCmdOutMsg.read().deviceCmd == 0 + + configure_window(imaging, 0.0, 0.0) + imaging.controller.UpdateState(macros.sec2nano(115)) + + assert imaging.controller.deviceCmdOutMsg.read().deviceCmd == 1 + + +@pytest.mark.parametrize("constraint", ["pointing", "rate", "access"]) +def test_valid_time_window_does_not_bypass_other_constraints(imaging, constraint): + configure_window(imaging, 2.0, 3.0) + imaging.fsw.action_image([6378137, 0, 0], "images", imaging_time=10.0) + guidance = messaging.AttGuidMsgPayload() + access = messaging.AccessMsgPayload() + access.hasAccess = 1 + if constraint == "pointing": + guidance.sigma_BR = [0.1, 0, 0] + elif constraint == "rate": + guidance.omega_BR_B = [0.1, 0, 0] + else: + access.hasAccess = 0 + imaging.guidance.write(guidance) + imaging.access.write(access) + + imaging.controller.UpdateState(macros.sec2nano(110)) + + assert imaging.controller.deviceCmdOutMsg.read().deviceCmd == 0 + + +def test_time_window_keeps_one_capture_per_task(imaging): + configure_window(imaging, 2.0, 3.0) + imaging.fsw.action_image([6378137, 0, 0], "images", imaging_time=10.0) + commands = [] + for time_s in (107, 108, 109, 110, 113, 114): + imaging.controller.UpdateState(macros.sec2nano(time_s)) + commands.append(imaging.controller.deviceCmdOutMsg.read().deviceCmd) + assert commands == [0, 1, 0, 0, 0, 0] + + imaging.fsw.simulator.sim_time = 120.0 + imaging.fsw.action_image([6378137, 0, 0], "images", imaging_time=10.0) + imaging.controller.UpdateState(macros.sec2nano(128)) + assert imaging.controller.deviceCmdOutMsg.read().deviceCmd == 1 diff --git a/tests/integration/test_int_gym_env.py b/tests/integration/test_int_gym_env.py index 8e4a7790..29ca9c41 100644 --- a/tests/integration/test_int_gym_env.py +++ b/tests/integration/test_int_gym_env.py @@ -2,10 +2,13 @@ import gymnasium as gym import numpy as np +import pytest from gymnasium import spaces -from bsk_rl import act, data, obs, sats +from bsk_rl import ConstellationTasking, act, data, obs, sats +from bsk_rl.gym import NO_ACTION from bsk_rl.scene import UniformTargets +from bsk_rl.sim import dyn, fsw from bsk_rl.utils.orbital import random_orbit @@ -14,6 +17,124 @@ class DoNothingSatellite(sats.ImagingSatellite): action_spec: ClassVar[list[act.Action]] = [act.Drift()] +class HybridModeSatellite(sats.Satellite): + dyn_type = dyn.BasicDynamicsModel + fsw_type = fsw.BasicFSWModel + observation_spec: ClassVar[list[obs.Observation]] = [obs.Time()] + action_spec: ClassVar[list[act.Action]] = [ + act.Charge(hybrid=True), + act.Drift(hybrid=True), + ] + + +@pytest.fixture +def make_hybrid_constellation(): + environments = [] + + def make_env(n_satellites=2, only_retask_idle=False): + names = [f"hybrid_{i}" for i in range(n_satellites)] + env = ConstellationTasking( + satellites=[HybridModeSatellite(name) for name in names], + scenario=UniformTargets(n_targets=0), + rewarder=data.NoReward(), + meta_agent_groupings={"group": names}, + only_retask_idle_meta_agent_members=only_retask_idle, + sim_rate=1.0, + max_step_duration=100.0, + time_limit=100.0, + ) + environments.append(env) + return env + + yield make_env + + for env in environments: + env.close() + + +@pytest.mark.parametrize("n_satellites", [1, 2]) +def test_constellation_sampled_hybrid_actions(make_hybrid_constellation, n_satellites): + env = make_hybrid_constellation(n_satellites=n_satellites) + env.reset(seed=0) + space = env.action_space("group") + space.seed(42) + action = space.sample() + + env.step({"group": action}) + + assert env.simulator.sim_time > 0 + member_actions = (action,) if n_satellites == 1 else action + for satellite, member_action in zip( + env.meta_agent_groupings["group"], member_actions + ): + assert satellite.action_builder.prev_action_key == ( + "action_charge" if member_action[0] == 0 else "action_drift" + ) + + +@pytest.mark.parametrize("skip_first", [False, True]) +def test_constellation_hybrid_object_array_actions( + make_hybrid_constellation, skip_first +): + env = make_hybrid_constellation() + env.reset(seed=0) + first, second = env.meta_agent_groupings["group"] + action = np.empty(2, dtype=object) + action[0] = None if skip_first else (0, np.array([2.0])) + action[1] = (1, np.array([5.0])) + + env.step({"group": action}) + + assert env.simulator.sim_time == pytest.approx(5.0 if skip_first else 2.0) + assert first.action_builder.prev_action_key == ( + None if skip_first else "action_charge" + ) + assert second.action_builder.prev_action_key == "action_drift" + + +def test_constellation_hybrid_idle_filter_preserves_deadline(make_hybrid_constellation): + env = make_hybrid_constellation(only_retask_idle=True) + env.reset(seed=0) + first, second = env.meta_agent_groupings["group"] + + env.step({"group": ((0, np.array([2.0])), (1, np.array([5.0])))}) + + assert env.simulator.sim_time == pytest.approx(2.0) + assert first.requires_retasking + assert not second.requires_retasking + + env.step({"group": ((1, np.array([8.0])), (0, np.array([10.0])))}) + + assert env.simulator.sim_time == pytest.approx(5.0) + assert first.action_builder.prev_action_key == "action_drift" + assert second.action_builder.prev_action_key == "action_drift" + assert not first.requires_retasking + assert second.requires_retasking + + +@pytest.mark.parametrize( + "idle_action", + [None, NO_ACTION, ((NO_ACTION, np.array([NO_ACTION], dtype=np.float32)),) * 2], + ids=["none", "scalar_sentinel", "nested_sentinel"], +) +def test_constellation_hybrid_wait_preserves_running_tasks( + make_hybrid_constellation, idle_action +): + env = make_hybrid_constellation() + env.reset(seed=0) + first, second = env.meta_agent_groupings["group"] + env.step({"group": ((0, np.array([2.0])), (1, np.array([5.0])))}) + assert env.simulator.sim_time == pytest.approx(2.0) + + env.step({"group": idle_action}) + + assert env.simulator.sim_time == pytest.approx(5.0) + assert first.action_builder.prev_action_key == "action_charge" + assert second.action_builder.prev_action_key == "action_drift" + assert first.requires_retasking + assert second.requires_retasking + + class TestSatelliteTasking: env = gym.make( "SatelliteTasking-v1", diff --git a/tests/unittest/act/test_actions.py b/tests/unittest/act/test_actions.py index 848ef769..a37fbb67 100644 --- a/tests/unittest/act/test_actions.py +++ b/tests/unittest/act/test_actions.py @@ -1,4 +1,5 @@ -from unittest.mock import MagicMock, call, patch +from types import SimpleNamespace +from unittest.mock import MagicMock, call, create_autospec, patch import numpy as np import pytest @@ -10,6 +11,15 @@ from bsk_rl.act.discrete_actions import DiscreteActionBuilder +@pytest.fixture +def imaging_satellite(): + satellite = MagicMock() + satellite.fsw.insControl = SimpleNamespace( + timeToleranceLower=0, timeToleranceUpper=0, imagingTime=0 + ) + return satellite + + @patch.multiple(ActionBuilder, __abstractmethods__=set()) class TestActionBuilder: def test_init(self): @@ -32,9 +42,22 @@ def test_action_space(self): satellite = MagicMock( action_spec=[MagicMock(n_actions=1), MagicMock(n_actions=2)] ) + for action in satellite.action_spec: + action.hybrid = False ab = DiscreteActionBuilder(satellite) assert ab.action_space == spaces.Discrete(3) + def test_hybrid_action_space(self): + satellite = MagicMock( + action_spec=[MagicMock(n_actions=1), MagicMock(n_actions=2)] + ) + for action in satellite.action_spec: + action.hybrid = True + ab = DiscreteActionBuilder(satellite) + assert ab.action_space == spaces.Tuple( + (spaces.Discrete(3), spaces.Box(low=0, high=np.inf, shape=(1,))) + ) + def test_action_description(self): satellite = MagicMock( action_spec=[ @@ -57,13 +80,21 @@ def test_set_action(self): ) ab = DiscreteActionBuilder(satellite) ab.set_action(0) - assert ab.action_spec[0].set_action.call_args == call(0, prev_action_key=None) + assert ab.action_spec[0].set_action.call_args == call( + (0, None), prev_action_key=None + ) ab.set_action(1) - assert ab.action_spec[1].set_action.call_args == call(0, prev_action_key="foo") + assert ab.action_spec[1].set_action.call_args == call( + (0, None), prev_action_key="foo" + ) ab.set_action(2) - assert ab.action_spec[1].set_action.call_args == call(1, prev_action_key="bar") + assert ab.action_spec[1].set_action.call_args == call( + (1, None), prev_action_key="bar" + ) ab.set_action(3) - assert ab.action_spec[2].set_action.call_args == call(0, prev_action_key="bar") + assert ab.action_spec[2].set_action.call_args == call( + (0, None), prev_action_key="bar" + ) def test_set_action_override(self): satellite = MagicMock( @@ -75,7 +106,7 @@ def test_set_action_override(self): ab = DiscreteActionBuilder(satellite) ab.set_action("foo") assert ab.action_spec[1].set_action_override.call_args == call( - "foo", prev_action_key=None + ("foo", None), prev_action_key=None ) @@ -83,72 +114,368 @@ class TestDiscreteFSWAction: def test_set_action(self): fswact = act.DiscreteFSWAction("action_fsw") fswact.satellite = MagicMock() - fswact.simulator = MagicMock() + fswact.simulator = SimpleNamespace(sim_rate=1.0, sim_time=0.0) fswact.set_action(0) fswact.satellite.fsw.action_fsw.assert_called_once() def test_set_action_again(self): fswact = act.DiscreteFSWAction("action_fsw") fswact.satellite = MagicMock() - fswact.simulator = MagicMock() + fswact.simulator = SimpleNamespace(sim_rate=1.0, sim_time=0.0) fswact.set_action(0, prev_action_key="action_fsw") fswact.satellite.fsw.action_fsw.assert_not_called() def test_set_action_reset(self): fswact = act.DiscreteFSWAction("action_fsw", reset_task=True) fswact.satellite = MagicMock() - fswact.simulator = MagicMock() + fswact.simulator = SimpleNamespace(sim_rate=1.0, sim_time=0.0) fswact.set_action(0, prev_action_key="action_fsw") fswact.satellite.fsw.action_fsw.assert_called_once() +@pytest.mark.parametrize("kind", ["fsw", "broadcast"]) +@pytest.mark.parametrize( + "requested,cap,sim_rate,expected_duration", + [ + (0.0, 100.0, 1.0, 1.0), + (0.25, 100.0, 1.0, 1.0), + (5.0, 100.0, 1.0, 5.0), + (20.0, 10.0, 1.0, 10.0), + (0.1, 0.25, 1.0, 1.0), + (None, 0.25, 1.0, 1.0), + (None, 10.0, 1.0, 10.0), + (0.25, 100.0, 0.5, 0.5), + ], +) +def test_hybrid_mode_action_duration(kind, requested, cap, sim_rate, expected_duration): + if kind == "fsw": + action = act.DiscreteFSWAction("action_fsw", duration=cap, hybrid=True) + else: + action = act.Broadcast(duration=cap, hybrid=True) + action.satellite = MagicMock() + action.simulator = SimpleNamespace(sim_rate=sim_rate, sim_time=10.0) + + action.set_action((0, requested)) + + action.satellite.update_timed_terminal_event.assert_called_once() + assert action.satellite.update_timed_terminal_event.call_args.args[ + 0 + ] == pytest.approx(10.0 + expected_duration) + if kind == "fsw": + action.satellite.logger.info.assert_called_once_with( + f"{action.name} tasked for {expected_duration} seconds" + ) + + +@pytest.fixture +def make_image(imaging_satellite): + def make(*, hybrid=True, max_duration=None, sim_rate=1.0, lower=0, upper=0): + image = act.Image(n_ahead_image=10, max_duration=max_duration, hybrid=hybrid) + image.satellite = imaging_satellite + image.simulator = SimpleNamespace(sim_rate=sim_rate, sim_time=100.0) + image.satellite.fsw.insControl.timeToleranceLower = int(lower * 1e9) + image.satellite.fsw.insControl.timeToleranceUpper = int(upper * 1e9) + image.satellite.parse_target_selection.return_value = SimpleNamespace( + id="target_1" + ) + image.reset_post_sim_init() + image.satellite.logger.reset_mock() + return image + + return make + + class TestImage: - target = MagicMock() - target.id = "target_1" - - def test_image(self): - image = act.Image(n_ahead_image=10) - image.satellite = MagicMock() - image.satellite.parse_target_selection.return_value = self.target - out = image.image(5, None) + # image() is shared by discrete and hybrid actions; only the entry points + # below need to vary the hybrid flag. + def test_image(self, make_image): + image = make_image() + target = image.satellite.parse_target_selection.return_value + + assert image.image(5) == target.id image.satellite.task_target_for_imaging.assert_called_once_with( - self.target, max_duration=None + target, max_duration=None + ) + + @pytest.mark.parametrize("hybrid", [False, True]) + def test_image_legacy_satellite_override(self, make_image, hybrid): + image = make_image(hybrid=hybrid) + target = image.satellite.parse_target_selection.return_value + image.satellite.task_target_for_imaging = create_autospec( + lambda target, max_duration=None: None ) - assert out == "target_1" - def test_image_retask(self): - image = act.Image(n_ahead_image=10) - image.satellite = MagicMock() - image.satellite.parse_target_selection.return_value = self.target - out = image.image(5, "target_1") + assert image.set_action(5) == target.id + image.satellite.task_target_for_imaging.assert_called_once_with( + target, max_duration=None + ) + + def test_image_retask(self, make_image): + image = make_image() + target = image.satellite.parse_target_selection.return_value + + assert image.image(5, target.id) == target.id image.satellite.enable_target_window.assert_called_once_with( - self.target, max_duration=None + target, max_duration=None ) - assert out == "target_1" + image.satellite.task_target_for_imaging.assert_not_called() - def test_image_max_duration(self): - image = act.Image(n_ahead_image=10, max_duration=100.0) - image.satellite = MagicMock() - image.satellite.parse_target_selection.return_value = self.target - out = image.image(5, None) + def test_image_max_duration(self, make_image): + image = make_image(max_duration=100.0) + target = image.satellite.parse_target_selection.return_value + + assert image.image(5) == target.id image.satellite.task_target_for_imaging.assert_called_once_with( - self.target, max_duration=100.0 + target, max_duration=100.0 ) - assert out == "target_1" - def test_set_action(self): - image = act.Image(n_ahead_image=10) - image.satellite = MagicMock() - image.image = MagicMock() - image.set_action(5) - image.image.assert_called_once_with(5, None) + @pytest.mark.parametrize("imaging_time", [0.0, 20.0]) + def test_image_forwards_requested_time(self, make_image, imaging_time): + image = make_image() + target = image.satellite.parse_target_selection.return_value - def test_set_action_override(self): - image = act.Image(n_ahead_image=10) - image.satellite = MagicMock() + assert image.image(5, imaging_time=imaging_time) == target.id + image.satellite.task_target_for_imaging.assert_called_once_with( + target, max_duration=None, imaging_time=imaging_time + ) + + @pytest.mark.parametrize( + "imaging_time,upper,cap,sim_rate,expected_duration,prev_action_key", + [ + (20.0, 3.0, None, 1.0, 23.0, None), + (20.0, 3.0, 10.0, 1.0, 10.0, None), + (20.0, 3.0, 100.0, 1.0, 23.0, None), + (0.0, 0.25, None, 1.0, 1.0, None), + (20.0, 3.0, 0.25, 1.0, 1.0, None), + (0.0, 0.25, None, 0.5, 0.5, None), + (20.0, 0.0, None, 1.0, None, None), + (20.0, 0.0, 100.0, 1.0, 100.0, None), + (None, 3.0, None, 1.0, None, None), + (None, 3.0, 100.0, 1.0, 100.0, None), + # The deadline calculation is shared. Retasking needs representative + # timed and untimed cases to check the separate forwarding path. + (0.0, 0.25, None, 1.0, 1.0, "target_1"), + (None, 3.0, 100.0, 1.0, 100.0, "target_1"), + ], + ) + def test_imaging_deadline( + self, + make_image, + imaging_time, + upper, + cap, + sim_rate, + expected_duration, + prev_action_key, + ): + image = make_image(max_duration=cap, sim_rate=sim_rate, upper=upper) + target = image.satellite.parse_target_selection.return_value + + assert ( + image.image(5, prev_action_key=prev_action_key, imaging_time=imaging_time) + == target.id + ) + + if prev_action_key is None: + kwargs = {} if imaging_time is None else {"imaging_time": imaging_time} + image.satellite.task_target_for_imaging.assert_called_once_with( + target, max_duration=expected_duration, **kwargs + ) + image.satellite.enable_target_window.assert_not_called() + else: + image.satellite.enable_target_window.assert_called_once_with( + target, max_duration=expected_duration + ) + image.satellite.task_target_for_imaging.assert_not_called() + if imaging_time is None: + image.satellite.update_imaging_time.assert_not_called() + else: + image.satellite.update_imaging_time.assert_called_once_with( + imaging_time=imaging_time + ) + + @pytest.mark.parametrize("prev_action_key", [None, "target_1"]) + @pytest.mark.parametrize( + "lower,upper,imaging_time,expect_warning", + [ + (0, 0, None, False), + (2, 0, None, True), + (0, 3, None, True), + (2, 3, None, True), + (2, 3, 0.0, False), + (2, 3, 20.0, False), + ], + ) + def test_missing_imaging_time_warning( + self, + make_image, + recwarn, + prev_action_key, + lower, + upper, + imaging_time, + expect_warning, + ): + image = make_image(lower=lower, upper=upper) + target = image.satellite.parse_target_selection.return_value + controller = image.satellite.fsw.insControl + controller.imagingTime = int(100 * 1e9) + previous_timing = vars(controller).copy() + + assert ( + image.image(5, prev_action_key=prev_action_key, imaging_time=imaging_time) + == target.id + ) + + assert not recwarn + if expect_warning: + image.satellite.logger.warning.assert_called_once() + assert ( + "existing imagingTime and tolerances are retained" + in (image.satellite.logger.warning.call_args.args[0]) + ) + else: + image.satellite.logger.warning.assert_not_called() + if imaging_time is None: + assert vars(controller) == previous_timing + image.satellite.update_imaging_time.assert_not_called() + if prev_action_key is None: + image.satellite.task_target_for_imaging.assert_called_once_with( + target, max_duration=None + ) + + @pytest.mark.parametrize( + "hybrid,lower,keyword", + [ + (False, 0, "max_duration"), + (True, 0, "max_duration"), + (True, 1, "imaging_time"), + ], + ids=["discrete", "hybrid_duration", "hybrid_capture_time"], + ) + @pytest.mark.parametrize( + "selection,method", [(5, "set_action"), ("image", "set_action_override")] + ) + @pytest.mark.parametrize("imaging_time", [None, 20.0]) + def test_action_entry_points( + self, make_image, hybrid, lower, keyword, selection, method, imaging_time + ): + image = make_image(hybrid=hybrid, lower=lower) image.image = MagicMock() - image.set_action_override("image") - image.image.assert_called_once_with("image", None) + payload = selection if imaging_time is None else (selection, imaging_time) + + getattr(image, method)(payload) + + image.image.assert_called_once_with( + selection, None, **{keyword: imaging_time if hybrid else None} + ) + + @pytest.mark.parametrize( + "requested,cap,sim_rate,expected_duration", + [ + (0.0, None, 1.0, 1.0), + (0.25, None, 1.0, 1.0), + (5.0, None, 1.0, 5.0), + (20.0, 10.0, 1.0, 10.0), + (0.1, 0.25, 1.0, 1.0), + (0.25, None, 0.5, 0.5), + (None, 10.0, 1.0, 10.0), + (None, None, 1.0, None), + ], + ) + def test_duration_mode_limits_wait_without_setting_capture_time( + self, + make_image, + requested, + cap, + sim_rate, + expected_duration, + ): + image = make_image(max_duration=cap, sim_rate=sim_rate) + target = image.satellite.parse_target_selection.return_value + controller = image.satellite.fsw.insControl + controller.imagingTime = int(200e9) + + assert image.set_action((5, requested)) == target.id + + image.satellite.task_target_for_imaging.assert_called_once_with( + target, max_duration=expected_duration + ) + assert controller.imagingTime == int(200e9) + assert image.max_duration == cap + image.satellite.logger.warning.assert_not_called() + + def test_duration_retask_does_not_update_capture_time(self, make_image): + image = make_image(max_duration=10.0) + target = image.satellite.parse_target_selection.return_value + + assert ( + image.set_action_override((target.id, 0.0), prev_action_key=target.id) + == target.id + ) + + image.satellite.enable_target_window.assert_called_once_with( + target, max_duration=1.0 + ) + image.satellite.update_imaging_time.assert_not_called() + image.satellite.task_target_for_imaging.assert_not_called() + assert image.max_duration == 10.0 + + +@pytest.mark.parametrize("lower,upper", [(0, 0), (1, 0), (0, 2), (1, 2)]) +@pytest.mark.parametrize("hybrid", [False, True]) +def test_image_mode_warning_only_for_hybrid_capture_time( + imaging_satellite, lower, upper, hybrid +): + image = act.Image(1, hybrid=hybrid) + image.satellite = imaging_satellite + controller = imaging_satellite.fsw.insControl + controller.timeToleranceLower = int(lower * 1e9) + controller.timeToleranceUpper = int(upper * 1e9) + + image.reset_post_sim_init() + + assert image._use_capture_time is bool(lower or upper) + assert image._time_tolerance_upper == pytest.approx(upper) + if hybrid and (lower or upper): + imaging_satellite.logger.warning.assert_called_once() + assert ( + "rather than a maximum duration" + in imaging_satellite.logger.warning.call_args.args[0] + ) + else: + imaging_satellite.logger.warning.assert_not_called() + + +def test_image_mode_refreshes_at_reset(imaging_satellite): + image = act.Image(1, hybrid=True) + image.satellite = imaging_satellite + controller = imaging_satellite.fsw.insControl + image.reset_post_sim_init() + assert not image._use_capture_time + + controller.timeToleranceUpper = int(2e9) + image.reset_post_sim_init() + assert image._use_capture_time + assert image._time_tolerance_upper == 2.0 + + controller.timeToleranceUpper = 0 + image.reset_post_sim_init() + assert not image._use_capture_time + assert image._time_tolerance_upper == 0.0 + imaging_satellite.logger.warning.assert_called_once() + + +def test_image_without_controller_tolerances_uses_duration_mode(imaging_satellite): + image = act.Image(1, hybrid=True) + image.satellite = imaging_satellite + imaging_satellite.fsw.insControl = SimpleNamespace() + + image.reset_post_sim_init() + + assert not image._use_capture_time + assert image._time_tolerance_upper == 0.0 + imaging_satellite.logger.warning.assert_not_called() class TestContinuousActionBuilder: diff --git a/tests/unittest/act/test_hybrid_actions.py b/tests/unittest/act/test_hybrid_actions.py new file mode 100644 index 00000000..a2173c54 --- /dev/null +++ b/tests/unittest/act/test_hybrid_actions.py @@ -0,0 +1,248 @@ +"""Hybrid parameters must survive normalization and mixed action dispatch.""" + +from types import SimpleNamespace +from unittest.mock import Mock + +import numpy as np +import pytest +from gymnasium import spaces + +from bsk_rl import act +from bsk_rl.act.discrete_actions import DiscreteActionBuilder, clean_action +from bsk_rl.scene.targets import Target + + +@pytest.mark.parametrize( + "parameter,expected", + [ + (0, 0.0), + (2.5, 2.5), + (np.float32(2.5), 2.5), + (np.array(2.5), 2.5), + (np.array([2.5], dtype=np.float32), 2.5), + ([2.5], 2.5), + ((2.5,), 2.5), + (None, None), + ], +) +def test_clean_action_normalizes_parameter(parameter, expected): + selected, value = clean_action((2, parameter), hybrid=True) + + assert selected == 2 + assert value == expected + if value is not None: + assert type(value) is float + + +@pytest.mark.parametrize("selection", [np.int64(2), "target_id", object()]) +def test_clean_action_preserves_selection(selection): + selected, value = clean_action((selection, 2.5), hybrid=True) + + assert selected is selection + assert value == 2.5 + + +@pytest.mark.parametrize("hybrid", [False, True]) +def test_clean_action_without_parameter(hybrid): + assert clean_action(2, hybrid=hybrid) == (2, None) + + +@pytest.mark.parametrize("parameter", [[], [1.0, 2.0], [[1.0]], np.ones((1, 1))]) +def test_clean_action_rejects_non_scalar_parameters(parameter): + with pytest.raises(ValueError, match="scalar or one-element"): + clean_action((0, parameter), hybrid=True) + + +@pytest.mark.parametrize( + "parameter", + [-1.0, np.array([np.nan]), np.inf, np.array([-np.inf])], + ids=["negative", "nan", "positive_infinity", "negative_infinity"], +) +def test_clean_action_rejects_invalid_parameter_values(parameter): + with pytest.raises(ValueError, match="finite and nonnegative"): + clean_action((0, parameter), hybrid=True) + + +@pytest.mark.parametrize("action", [(), (0,), (0, 1.0, 2.0)]) +def test_clean_action_rejects_malformed_tuples(action): + with pytest.raises(ValueError): + clean_action(action, hybrid=True) + + +def test_nonhybrid_action_discards_continuous_parameter(): + assert clean_action((2, np.array([3.0, 4.0])), hybrid=False) == (2, None) + + +@pytest.fixture +def mixed_builder(): + targets = [Target(f"target_{i}", [1, 0, 0], 1.0) for i in range(2)] + simulator = SimpleNamespace(sim_time=30.0, sim_rate=1.0) + satellite = SimpleNamespace( + action_spec=[ + act.Charge(duration=12.0), + act.Image(n_ahead_image=2, hybrid=True), + act.Broadcast(duration=9.0, hybrid=True), + ], + simulator=simulator, + fsw=SimpleNamespace( + action_charge=Mock(), + insControl=SimpleNamespace(timeToleranceLower=0, timeToleranceUpper=0), + ), + logger=Mock(), + disable_timed_terminal_event=Mock(), + update_timed_terminal_event=Mock(), + task_target_for_imaging=Mock(), + enable_target_window=Mock(), + update_imaging_time=Mock(), + ) + + def select_target(selection): + if isinstance(selection, (int, np.integer)): + return targets[selection] + return next( + target + for target in targets + if selection is target or selection == target.id + ) + + satellite.parse_target_selection = select_target + builder = DiscreteActionBuilder(satellite) + builder.reset_post_sim_init() + return builder, satellite, targets + + +def test_mixed_action_space_and_sample(mixed_builder): + builder, satellite, _ = mixed_builder + space = builder.action_space + assert isinstance(space, spaces.Tuple) + assert space[0].n == 4 + assert space[1].shape == (1,) + space.seed(42) + action = space.sample() + assert space.contains(action) + + builder.set_action(action) + + satellite.disable_timed_terminal_event.assert_called_once() + assert builder.prev_action_key is not None + + +@pytest.mark.parametrize("global_index,local_index", [(1, 0), (np.int64(2), 1)]) +@pytest.mark.parametrize("timed", [False, True]) +def test_mixed_builder_dispatches_global_image_index( + mixed_builder, global_index, local_index, timed +): + builder, satellite, targets = mixed_builder + if timed: + satellite.fsw.insControl.timeToleranceLower = int(1e9) + builder.reset_post_sim_init() + + builder.set_action((global_index, np.array([2.5], dtype=np.float32))) + + satellite.task_target_for_imaging.assert_called_once_with( + targets[local_index], + **( + {"max_duration": None, "imaging_time": 2.5} + if timed + else {"max_duration": 2.5} + ), + ) + satellite.fsw.action_charge.assert_not_called() + assert not builder.action_spec[2].broadcast_pending + assert builder.prev_action_key == targets[local_index].id + + +@pytest.mark.parametrize("as_object", [False, True]) +def test_mixed_builder_forwards_target_override(mixed_builder, as_object): + builder, satellite, targets = mixed_builder + target = targets[1] + + builder.set_action((target if as_object else target.id, np.array([0.0]))) + + satellite.task_target_for_imaging.assert_called_once_with(target, max_duration=1.0) + assert builder.prev_action_key == target.id + + +def test_mixed_builder_retasks_same_target_without_restarting_fsw(mixed_builder): + builder, satellite, targets = mixed_builder + builder.set_action((1, 5.0)) + builder.set_action((1, np.array([0.0]))) + + satellite.task_target_for_imaging.assert_called_once() + satellite.enable_target_window.assert_called_once_with(targets[0], max_duration=1.0) + satellite.update_imaging_time.assert_not_called() + + +def test_nonhybrid_override_discards_parameter_in_mixed_builder(mixed_builder): + builder, satellite, targets = mixed_builder + builder.action_spec[1].hybrid = False + # Broadcast still makes the overall space hybrid. + assert isinstance(builder.action_space, spaces.Tuple) + + builder.set_action((targets[0].id, np.array([2.5]))) + + satellite.task_target_for_imaging.assert_called_once_with( + targets[0], max_duration=None + ) + + +def test_mixed_builder_accepts_legacy_integer_for_hybrid_action(mixed_builder): + builder, satellite, targets = mixed_builder + + builder.set_action(1) + + satellite.task_target_for_imaging.assert_called_once_with( + targets[0], max_duration=None + ) + + +def test_mixed_builder_keeps_fixed_duration_for_nonhybrid_action(mixed_builder): + builder, satellite, _ = mixed_builder + + builder.set_action((0, np.array([2.5]))) + + satellite.fsw.action_charge.assert_called_once() + satellite.update_timed_terminal_event.assert_called_once_with( + 42.0, info="for action_charge" + ) + + +def test_mixed_builder_dispatches_and_caps_broadcast(mixed_builder): + builder, satellite, _ = mixed_builder + + builder.set_action((3, np.array([20.0]))) + + assert builder.action_spec[2].broadcast_pending + satellite.update_timed_terminal_event.assert_called_once_with( + 39.0, info="for broadcast" + ) + + +@pytest.mark.parametrize("index", [-1, np.int64(-1), 4, 100]) +def test_mixed_builder_rejects_out_of_range_index_without_tasking(mixed_builder, index): + builder, satellite, _ = mixed_builder + + with pytest.raises(ValueError, match="out of range"): + builder.set_action((index, np.array([2.5]))) + + satellite.fsw.action_charge.assert_not_called() + satellite.task_target_for_imaging.assert_not_called() + satellite.update_timed_terminal_event.assert_not_called() + assert builder.prev_action_key is None + + +@pytest.mark.parametrize( + "action_type", + [act.Charge, act.Drift, act.NadirPoint, act.Desat, act.Downlink, act.Scan], +) +def test_mode_actions_accept_sampled_hybrid_duration(action_type): + action = action_type(duration=10.0, hybrid=True) + action.satellite = Mock() + action.simulator = SimpleNamespace(sim_rate=0.5, sim_time=30.0) + + action.set_action((np.int64(0), np.array([2.5], dtype=np.float32))) + + getattr(action.satellite.fsw, action.fsw_action).assert_called_once() + action.satellite.update_timed_terminal_event.assert_called_once_with( + 32.5, info=f"for {action.fsw_action}" + ) diff --git a/tests/unittest/act/test_rso_imaging_actions.py b/tests/unittest/act/test_rso_imaging_actions.py index 746c8966..6026cfb5 100644 --- a/tests/unittest/act/test_rso_imaging_actions.py +++ b/tests/unittest/act/test_rso_imaging_actions.py @@ -3,6 +3,7 @@ from types import SimpleNamespace as NS from unittest.mock import Mock +import numpy as np import pytest from bsk_rl.act import ImageRSO @@ -110,3 +111,37 @@ def test_pending_target_tasking_respects_scenario_setting( action.satellite.update_timed_terminal_event.assert_called_once() assert action.chosen_target_ids == [definition.id] assert mission.pending[definition.id] == {"camera:1"} + + +@pytest.mark.parametrize("parameter", [None, 0.0, np.array([2.5], dtype=np.float32)]) +def test_nonhybrid_rso_accepts_tuple_without_forwarding_parameter(parameter): + action = ImageRSO(1) + target = NS(id="observed") + action.satellite = NS( + rso_scenario=NS(revision=0, targets_by_id={target.id: target}), + _rso_candidate_snapshot=CandidateSnapshot(10, 0, (target,)), + ) + action.simulator = NS(sim_time=10) + action._task = Mock(return_value=target.id) + + assert action.set_action((0, parameter)) == target.id + + action._task.assert_called_once_with(target) + assert action.hybrid is False + + +@pytest.mark.parametrize("as_object", [False, True]) +def test_nonhybrid_rso_normalizes_explicit_target_tuple(as_object): + action = ImageRSO(1) + target = NS(id="observed") + action.satellite = NS(rso_scenario=NS(targets_by_id={target.id: target})) + action._task = Mock(return_value=target.id) + + assert ( + action.set_action_override( + (target if as_object else target.id, np.array([2.5])) + ) + == target.id + ) + + action._task.assert_called_once_with(target) diff --git a/tests/unittest/sats/test_access_satellite.py b/tests/unittest/sats/test_access_satellite.py index 199a2132..5d68de64 100644 --- a/tests/unittest/sats/test_access_satellite.py +++ b/tests/unittest/sats/test_access_satellite.py @@ -1,6 +1,6 @@ from collections.abc import Iterable from typing import ClassVar -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, create_autospec, patch import numpy as np import pytest @@ -463,10 +463,38 @@ def test_task_target_for_imaging(self): sat.update_timed_terminal_event = MagicMock() sat.logger = MagicMock() sat.task_target_for_imaging(self.tgt0) - sat.fsw.action_image.assert_called_once() + sat.fsw.action_image.assert_called_once_with(self.tgt0.r_LP_P, sat.buffer_name) sat.logger.info.assert_called() sat._update_image_event.assert_called_once() assert sat._update_image_event.call_args[0][0] == self.tgt0 sat.update_timed_terminal_event.assert_called_once() assert sat.update_timed_terminal_event.call_args[0][0] == 50.0 assert sat.latest_target == self.tgt0 + + def test_task_target_for_imaging_legacy_fsw_override(self): + sat = self.make_sat() + sat.logger = MagicMock() + sat.fsw = MagicMock() + sat.fsw.action_image = create_autospec(lambda position, buffer_name: None) + sat.enable_target_window = MagicMock() + sat.draw_imaging_line = MagicMock() + + sat.task_target_for_imaging(self.tgt0, max_duration=100.0) + + sat.fsw.action_image.assert_called_once_with(self.tgt0.r_LP_P, sat.buffer_name) + sat.enable_target_window.assert_called_once_with(self.tgt0, max_duration=100.0) + assert sat.latest_target == self.tgt0 + + @pytest.mark.parametrize("imaging_time", [0.0, 20.0]) + def test_task_target_for_imaging_forwards_requested_time(self, imaging_time): + sat = self.make_sat() + sat.logger = MagicMock() + sat.fsw = MagicMock() + sat.enable_target_window = MagicMock() + sat.draw_imaging_line = MagicMock() + + sat.task_target_for_imaging(self.tgt0, imaging_time=imaging_time) + + sat.fsw.action_image.assert_called_once_with( + self.tgt0.r_LP_P, sat.buffer_name, imaging_time=imaging_time + ) diff --git a/tests/unittest/sim/test_fsw.py b/tests/unittest/sim/test_fsw.py index 37b18bef..2be3761d 100644 --- a/tests/unittest/sim/test_fsw.py +++ b/tests/unittest/sim/test_fsw.py @@ -1,6 +1,10 @@ +from types import SimpleNamespace from unittest.mock import MagicMock, patch import numpy as np +import pytest +from Basilisk.fswAlgorithms import simpleInstrumentController +from Basilisk.utilities import macros from bsk_rl.sim.fsw import BasicFSWModel, FSWModelABC, ImagingFSWModel, Task, action @@ -30,7 +34,6 @@ class TestFSWModel: def test_base_class(self): sat = MagicMock() fsw = FSWModelABC(sat, 1.0) - # fsw.simulator.CreateNewProcess.assert_called_once() assert sat.simulator.world == fsw.world assert sat.dynamics == fsw.dynamics @@ -83,3 +86,51 @@ def test_fsw_properties(self): fsw.locPoint = MagicMock(pHat_B=np.array([1.0, 0.0, 0.0])) fsw.satellite = MagicMock(dynamics=MagicMock(BP=np.identity(3))) assert (fsw.c_hat_P == np.array([1.0, 0.0, 0.0])).all() + + +class TestImagingTimeTolerance: + def test_unsupported_controller_logs_warning(self, caplog): + task = SimpleNamespace(insControl=object()) + + ImagingFSWModel.LocPointTask.setup_instrument_time_tolerance( + task, timeToleranceLower=2.0, timeToleranceUpper=3.0 + ) + + assert ( + "insControl.timeTolerance not available; continuing without it" + in caplog.text + ) + + @pytest.fixture + def task(self): + controller = simpleInstrumentController.simpleInstrumentController() + controller.timeToleranceLower = macros.sec2nano(11) + controller.timeToleranceUpper = macros.sec2nano(12) + return SimpleNamespace(insControl=controller) + + @pytest.mark.parametrize( + "lower,upper", [(0, 0), (0, 2.5), (1.25, 0), (1.25, 2.5), (1e-9, 2e-9)] + ) + def test_valid_tolerances(self, task, lower, upper): + ImagingFSWModel.LocPointTask.setup_instrument_time_tolerance( + task, timeToleranceLower=lower, timeToleranceUpper=upper + ) + + assert task.insControl.timeToleranceLower == macros.sec2nano(lower) + assert task.insControl.timeToleranceUpper == macros.sec2nano(upper) + + @pytest.mark.parametrize("name", ["timeToleranceLower", "timeToleranceUpper"]) + @pytest.mark.parametrize("value", [-1.0, -1e-9, np.nan, np.inf, -np.inf]) + def test_invalid_tolerance_preserves_both_bounds(self, task, caplog, name, value): + kwargs = {"timeToleranceLower": 2.0, "timeToleranceUpper": 3.0, name: value} + bound = "lower" if name == "timeToleranceLower" else "upper" + + with pytest.raises( + ValueError, + match=f"{name} must be finite and nonnegative; 0 disables the {bound} time bound", + ): + ImagingFSWModel.LocPointTask.setup_instrument_time_tolerance(task, **kwargs) + + assert task.insControl.timeToleranceLower == macros.sec2nano(11) + assert task.insControl.timeToleranceUpper == macros.sec2nano(12) + assert not caplog.records diff --git a/tests/unittest/test_gym_env.py b/tests/unittest/test_gym_env.py index 19670bc3..250b6ae6 100644 --- a/tests/unittest/test_gym_env.py +++ b/tests/unittest/test_gym_env.py @@ -1,3 +1,4 @@ +from types import SimpleNamespace from unittest.mock import MagicMock, patch import numpy as np @@ -29,12 +30,53 @@ (np.array([NO_ACTION - 1]), False), (np.array([]), False), (np.array([0.0, 1.0]), False), + ((0, np.array([5.0])), False), + (("target_id", np.array([5.0])), False), + ((NO_ACTION, np.array([NO_ACTION], dtype=np.float32)), True), + (((0, np.array([5.0])), (1, np.array([10.0]))), False), + ((None, (1, np.array([10.0]))), False), + ((None, (NO_ACTION, np.array([NO_ACTION]))), True), + ([], False), + ((), False), + (np.array(None, dtype=object), True), + (np.array(NO_ACTION, dtype=object), True), + (np.array(0, dtype=object), False), + (np.array([[None, NO_ACTION], [NO_ACTION, None]], dtype=object), True), + ([float(NO_ACTION)] * 64, True), + ((float(NO_ACTION),) * 64, True), + ([NO_ACTION] * 64, True), + ([NO_ACTION, NO_ACTION - 1], False), + ([float(NO_ACTION), float(NO_ACTION + 1)], True), + ([float(NO_ACTION), float(NO_ACTION + 2)], False), + ([float(NO_ACTION), NO_ACTION - 1], False), + ([NO_ACTION - 1, float(NO_ACTION)], False), + ([float(NO_ACTION), NO_ACTION], True), + ([float(NO_ACTION), np.float32(NO_ACTION)], True), ], ) def test_is_no_action(action, expected): assert is_no_action(action) is expected +@pytest.mark.parametrize( + "entries,expected", + [ + ([], False), + ([None, NO_ACTION], True), + ([(NO_ACTION, np.array([NO_ACTION], dtype=np.float32)), None], True), + ([(0, np.array([5.0])), (1, np.array([10.0]))], False), + ([None, (1, np.array([10.0]))], False), + ([np.array([None, NO_ACTION], dtype=object)], True), + ], +) +def test_is_no_action_structured_object_array(entries, expected): + action = np.empty(len(entries), dtype=object) + for i, entry in enumerate(entries): + action[i] = entry + + assert is_no_action(action) is expected + + def test_no_action_like(): assert no_action_like(0) == NO_ACTION assert np.allclose(no_action_like(np.array([1.0, 2.0])), NO_ACTION) @@ -61,6 +103,20 @@ def test_no_action_keep_indices(actions, action_idx): assert obs_idx == action_idx + [n_obs - 1] +def test_no_action_keep_indices_with_hybrid_actions(): + actions = [ + (0, np.array([5.0])), + None, + (NO_ACTION, np.array([NO_ACTION], dtype=np.float32)), + (1, np.array([2.0])), + ] + + action_idx, observation_idx = no_action_keep_indices(actions, n_observations=5) + + assert action_idx == [0, 3] + assert observation_idx == [0, 3, 4] + + class TypeA: pass @@ -456,6 +512,110 @@ def test_get_obs(self): assert env._get_obs() == env.satellite.get_obs() +class TestMetaActionDecomposition: + @staticmethod + def make_env(action_spaces, only_retask_idle=False): + satellites = [ + MagicMock(action_space=space, requires_retasking=True) + for space in action_spaces + ] + return SimpleNamespace( + meta_agent_groupings={"group": satellites}, + only_retask_idle_meta_agent_members=only_retask_idle, + dtype=None, + ), satellites + + @staticmethod + def hybrid_space(n=2): + return spaces.Tuple((spaces.Discrete(n), spaces.Box(0, np.inf, shape=(1,)))) + + @pytest.mark.parametrize("n_satellites", [1, 2, 3]) + def test_sampled_hybrid_actions(self, n_satellites): + env, satellites = self.make_env( + [self.hybrid_space(i + 2) for i in range(n_satellites)] + ) + space = ConstellationTasking.action_spaces.fget(env)["group"] + action = space.sample() + + result = ConstellationTasking._decompose_meta_action(env, "group", action) + + member_actions = (action,) if n_satellites == 1 else action + for satellite, member_action in zip(satellites, member_actions): + assert result[satellite] is member_action + + @pytest.mark.parametrize("enabled", [False, True]) + @pytest.mark.parametrize("kind", ["discrete", "box", "hybrid"]) + def test_idle_filter_preserves_member_positions(self, enabled, kind): + if kind == "hybrid": + action_spaces = [self.hybrid_space(), self.hybrid_space()] + expected = ((0, np.array([5.0])), (1, np.array([10.0]))) + action = expected + elif kind == "box": + action_spaces = [spaces.Box(-10, 10, (2,)), spaces.Box(-10, 10, (1,))] + action = np.array([1.0, 2.0, 3.0]) + expected = (np.array([1.0, 2.0]), 3.0) + else: + action_spaces = [spaces.Discrete(2), spaces.Discrete(3)] + action = np.array([1, 2]) + expected = (1, 2) + env, satellites = self.make_env(action_spaces, only_retask_idle=enabled) + satellites[0].requires_retasking = False + + result = ConstellationTasking._decompose_meta_action(env, "group", action) + + for i, satellite in enumerate(satellites): + if enabled and i == 0: + assert result[satellite] is None + elif kind == "hybrid": + assert result[satellite] is expected[i] + else: + np.testing.assert_array_equal(result[satellite], expected[i]) + + @pytest.mark.parametrize("n_satellites", [1, 2]) + @pytest.mark.parametrize( + "action", [None, NO_ACTION, (NO_ACTION, np.array([NO_ACTION]))] + ) + def test_hybrid_group_no_action(self, n_satellites, action): + env, satellites = self.make_env([self.hybrid_space()] * n_satellites) + + result = ConstellationTasking._decompose_meta_action(env, "group", action) + + assert result == dict.fromkeys(satellites) + + @pytest.mark.parametrize("idle_action", [None, NO_ACTION]) + def test_individual_hybrid_member_no_action(self, idle_action): + env, satellites = self.make_env([self.hybrid_space()] * 2) + active_action = (1, np.array([5.0])) + + result = ConstellationTasking._decompose_meta_action( + env, "group", (idle_action, active_action) + ) + + assert result[satellites[0]] is idle_action + assert result[satellites[1]] is active_action + + @pytest.mark.parametrize("first_action", [None, NO_ACTION, (0, np.array([5.0]))]) + def test_structured_object_array_members(self, first_action): + env, satellites = self.make_env([self.hybrid_space()] * 2) + action = np.empty(2, dtype=object) + action[0] = first_action + action[1] = (1, np.array([10.0])) + + result = ConstellationTasking._decompose_meta_action(env, "group", action) + + assert result[satellites[0]] is action[0] + assert result[satellites[1]] is action[1] + + @pytest.mark.parametrize( + "action", [(), ((0, 5.0),), ((0, 5.0),) * 3, 0, np.array(0), "bad"] + ) + def test_invalid_hybrid_group_size(self, action): + env, _ = self.make_env([self.hybrid_space()] * 2) + + with pytest.raises(ValueError, match="one hybrid action per satellite"): + ConstellationTasking._decompose_meta_action(env, "group", action) + + class TestConstellationTasking: @patch("bsk_rl.gym.Simulator") @patch("bsk_rl.ConstellationTasking._get_obs") From 6d9313a886f786907696b2c711f6bd2c939a89e0 Mon Sep 17 00:00:00 2001 From: LorenzzoQM Date: Mon, 26 Oct 2026 20:54:16 -0600 Subject: [PATCH 3/4] [#363] Add example script --- examples/_default.rst | 1 + examples/hybrid_image_quality.ipynb | 3321 +++++++++++++++++++++++++++ 2 files changed, 3322 insertions(+) create mode 100644 examples/hybrid_image_quality.ipynb diff --git a/examples/_default.rst b/examples/_default.rst index a8945013..ca7d27a5 100644 --- a/examples/_default.rst +++ b/examples/_default.rst @@ -21,6 +21,7 @@ Earth Observation cloud_environment cloud_environment_with_reimaging + hybrid_image_quality aeos RSO Missions diff --git a/examples/hybrid_image_quality.ipynb b/examples/hybrid_image_quality.ipynb new file mode 100644 index 00000000..a8859a6b --- /dev/null +++ b/examples/hybrid_image_quality.ipynb @@ -0,0 +1,3321 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "hybrid-quality-00", + "metadata": {}, + "source": [ + "# Hybrid imaging with time-varying image quality\n", + "\n", + "This example shows how to set up an imaging environment with targets that have heterogeneous, time-varying rewards. The underlying environment accepts hybrid actions consisting of a target index and a desired acquisition delay. Here, a custom actor produces categorical logits over target/timing pairs, and a separate critic estimates the state value. Action wrappers convert the selected pair into a target index and delay in seconds. The final section demonstrates multi-satellite coordination through shared acquisition information and task intents. This example is part of an upcoming publication.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "hybrid-quality-01", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/lorenzzo/Local/repo_updates/issue_363/.venv_issue_363/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "from collections.abc import Callable, Iterable\n", + "from typing import Any, ClassVar\n", + "\n", + "import gymnasium as gym\n", + "import numpy as np\n", + "from Basilisk.architecture import bskLogging\n", + "from Basilisk.utilities import orbitalMotion\n", + "from scipy.interpolate import make_lsq_spline\n", + "\n", + "from bsk_rl import ConstellationTasking, SatelliteTasking, act, obs, sats\n", + "from bsk_rl.data.unique_image_data import (\n", + " GlobalReward,\n", + " UniqueImageData,\n", + " UniqueImageReward,\n", + " UniqueImageStore,\n", + ")\n", + "from bsk_rl.scene.targets import CityTargets, Target\n", + "from bsk_rl.sim import dyn, fsw\n", + "from bsk_rl.utils.orbital import random_circular_orbit\n", + "\n", + "bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING)\n", + "\n", + "ALTITUDE_KM = 800.0\n", + "EARTH_RADIUS = orbitalMotion.REQ_EARTH * 1e3\n", + "ORBIT_PERIOD = (\n", + " 2\n", + " * np.pi\n", + " * np.sqrt((orbitalMotion.REQ_EARTH + ALTITUDE_KM) ** 3 / orbitalMotion.MU_EARTH)\n", + ")\n", + "N_AHEAD = 40\n", + "SIM_RATE = 0.5\n", + "IMAGING_FRACTIONS = (0.0, 0.5, 1.0)\n", + "SPLINE_DEGREE = 2\n", + "SPLINE_KNOTS = np.r_[np.zeros(3), [1 / 3, 2 / 3], np.ones(3)]" + ] + }, + { + "cell_type": "markdown", + "id": "hybrid-quality-02", + "metadata": {}, + "source": [ + "## Targets and quality\n", + "\n", + "Acquisition quality can depend on viewing geometry or a stakeholder preference for the acquisition time. This example includes the paper's off-nadir and periodic quality models. The distance and glare models are omitted to keep the example compact.\n", + "\n", + "`QualityTargets` extends `CityTargets` by assigning a quality model and weight to each target. Setting `quality_weight=1.0` makes reward equal to priority times quality, while `quality_weight=0.0` makes reward depend only on priority. Setting `quality_weight=(0.0, 1.0)` samples a separate weight for each target on every reset. Choose a single model with `quality_types=(\"quality_off_nadir_angle\",)` or `quality_types=(\"quality_periodic\",)`, or sample uniformly from `quality_types=(\"quality_off_nadir_angle\", \"quality_periodic\")`. The absolute acquisition reward is\n", + "\n", + "$$r_i(t)=p_i\\big[1-w_{q,i}+w_{q,i}q_i(t)\\big],$$\n", + "\n", + "where $p_i$ is target priority, $w_{q,i}$ is its quality weight, and $q_i(t)\\in[0,1]$ is the quality at acquisition time. The rewarder below determines whether an acquisition receives this absolute reward or only its improvement over the best completed reward.\n", + "\n", + "Off-nadir quality uses the angle between the target's local vertical and its viewing direction: $q=\\mathrm{clip}(1-2\\theta/\\pi,0,1)$. Future samples use predicted geometric viewing directions; at the current time, realized instrument pointing is used. Periodic quality represents a known, time-varying acquisition preference, using one or two sinusoidal components with periods between 150 and 570 seconds. Frequencies in the code are angular frequencies in radians per second. These are illustrative acquisition-quality models, not a radiometric sensor simulation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cd228d9f", + "metadata": {}, + "outputs": [], + "source": [ + "def periodic_quality_function(t, amplitudes, frequencies, phases):\n", + " \"\"\"Evaluate a window-independent sum of periodic components.\n", + "\n", + " ``frequencies`` are angular frequencies in radians per second.\n", + " \"\"\"\n", + " t = np.asarray(t, dtype=float)\n", + " amplitudes = np.asarray(amplitudes, dtype=float)\n", + " frequencies = np.asarray(frequencies, dtype=float)\n", + " phases = np.asarray(phases, dtype=float)\n", + "\n", + " if amplitudes.ndim != 1 or frequencies.ndim != 1 or phases.ndim != 1:\n", + " raise ValueError(\"amplitudes, frequencies, and phases must be 1-D arrays.\")\n", + " if not (len(amplitudes) == len(frequencies) == len(phases)):\n", + " raise ValueError(\n", + " \"amplitudes, frequencies, and phases must have the same length.\"\n", + " )\n", + " if len(amplitudes) == 0:\n", + " raise ValueError(\"At least one periodic component is required.\")\n", + " if not (\n", + " np.all(np.isfinite(t))\n", + " and np.all(np.isfinite(amplitudes))\n", + " and np.all(np.isfinite(frequencies))\n", + " and np.all(np.isfinite(phases))\n", + " ):\n", + " raise ValueError(\"Periodic-function inputs must all be finite.\")\n", + " if np.any(frequencies <= 0):\n", + " raise ValueError(\"Periodic frequencies must be greater than zero.\")\n", + "\n", + " amplitude_sum = np.sum(np.abs(amplitudes))\n", + " if amplitude_sum <= 0:\n", + " raise ValueError(\"At least one periodic amplitude must be nonzero.\")\n", + "\n", + " quality = np.sum(\n", + " np.array(\n", + " [\n", + " amplitudes[i] * np.sin(frequencies[i] * t + phases[i])\n", + " for i in range(len(amplitudes))\n", + " ]\n", + " ),\n", + " axis=0,\n", + " )\n", + "\n", + " quality = (quality + amplitude_sum) / (2 * amplitude_sum)\n", + "\n", + " return quality.item() if np.ndim(quality) == 0 else quality\n", + "\n", + "\n", + "def quality_periodic(sat, tgt, t, amplitudes, frequencies, phases, **kwargs):\n", + "\n", + " return periodic_quality_function(\n", + " t,\n", + " amplitudes=amplitudes,\n", + " frequencies=frequencies,\n", + " phases=phases,\n", + " )\n", + "\n", + "\n", + "def _positions_at_times(sat, times, trajectory_r_BP_P=None):\n", + " \"\"\"Return predicted positions, using dynamics for the realized current time.\"\"\"\n", + " times = np.asarray(times, dtype=float)\n", + " current_mask = np.abs(times - sat.simulator.sim_time) <= 1e-6\n", + "\n", + " if times.ndim == 0:\n", + " if current_mask:\n", + " return np.asarray(sat.dynamics.r_BN_P, dtype=float)\n", + " elif np.all(current_mask):\n", + " return np.broadcast_to(\n", + " np.asarray(sat.dynamics.r_BN_P, dtype=float),\n", + " times.shape + (3,),\n", + " ).copy()\n", + "\n", + " if trajectory_r_BP_P is None:\n", + " trajectory_r_BP_P = sat.trajectory.r_BP_P\n", + " positions = np.asarray(trajectory_r_BP_P(times), dtype=float)\n", + "\n", + " if times.ndim > 0 and np.any(current_mask):\n", + " positions = positions.copy()\n", + " positions[current_mask] = sat.dynamics.r_BN_P\n", + " return positions\n", + "\n", + "\n", + "def quality_off_nadir_angle(sat, tgt, t, trajectory_r_BP_P=None, **kwargs):\n", + " \"\"\"Return quality based on the target-centered instrument viewing angle.\n", + "\n", + " At the current simulation time, use the realized instrument direction so\n", + " the reward reflects the actual imaging attitude. For future profile samples,\n", + " where the attitude is not yet known, use the geometric target-to-satellite\n", + " direction. Quality decreases linearly from one for a target directly below\n", + " the instrument to zero for a target viewed at its local horizon.\n", + " \"\"\"\n", + " t = np.asarray(t, dtype=float)\n", + " scalar_input = t.ndim == 0\n", + " r_BN_P = _positions_at_times(sat, t, trajectory_r_BP_P)\n", + "\n", + " target_position = np.asarray(tgt.r_LP_P, dtype=float)\n", + " target_radius = np.linalg.norm(target_position)\n", + " target_to_camera = r_BN_P - target_position\n", + " view_norm = np.linalg.norm(target_to_camera, axis=-1, keepdims=True)\n", + " if (\n", + " np.any(~np.isfinite(r_BN_P))\n", + " or not np.all(np.isfinite(target_position))\n", + " or target_radius <= 0.0\n", + " or np.any(view_norm <= 0.0)\n", + " ):\n", + " raise ValueError(\"Off-nadir geometry must contain finite, nonzero vectors.\")\n", + "\n", + " local_vertical_hat_P = target_position / target_radius\n", + " view_hat_P = target_to_camera / view_norm\n", + "\n", + " realized_mask = np.abs(t - sat.simulator.sim_time) <= 1e-6\n", + " if np.any(realized_mask):\n", + " c_hat_P = np.asarray(sat.fsw.c_hat_P, dtype=float)\n", + " if c_hat_P.shape != (3,) or not np.all(np.isfinite(c_hat_P)):\n", + " raise ValueError(\"c_hat_P must be a finite three-component vector.\")\n", + " pointing_norm = np.linalg.norm(c_hat_P)\n", + " if pointing_norm <= 0.0:\n", + " raise ValueError(\"c_hat_P must be nonzero.\")\n", + " if scalar_input:\n", + " view_hat_P = -c_hat_P / pointing_norm\n", + " else:\n", + " view_hat_P = view_hat_P.copy()\n", + " view_hat_P[realized_mask] = -c_hat_P / pointing_norm\n", + "\n", + " off_nadir = np.arccos(\n", + " np.clip(np.sum(view_hat_P * local_vertical_hat_P, axis=-1), -1.0, 1.0)\n", + " )\n", + "\n", + " quality = np.clip(1 - off_nadir / (np.pi / 2), 0.0, 1.0)\n", + " return quality.item() if scalar_input else quality\n", + "\n", + "\n", + "QUALITY_FUNCTIONS = {\n", + " \"quality_off_nadir_angle\": quality_off_nadir_angle,\n", + " \"quality_periodic\": quality_periodic,\n", + "}\n", + "\n", + "\n", + "def get_randomized_quality_function_params(quality_function_name):\n", + " if quality_function_name in [\"quality_off_nadir_angle\"]:\n", + " return {}\n", + " elif quality_function_name == \"quality_periodic\":\n", + " n_components = np.random.randint(1, 3)\n", + " amplitudes = np.random.uniform(0.1, 1.0, size=n_components)\n", + " frequencies = (\n", + " 2\n", + " * np.pi\n", + " * np.random.uniform(\n", + " 10.0 / 5700.0,\n", + " 1.0 / 150.0,\n", + " size=n_components,\n", + " )\n", + " )\n", + " phases = np.random.uniform(0.0, 2 * np.pi, size=n_components)\n", + " return {\n", + " \"amplitudes\": amplitudes,\n", + " \"frequencies\": frequencies,\n", + " \"phases\": phases,\n", + " }\n", + " else:\n", + " raise ValueError(f\"Unknown quality function: {quality_function_name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "a17776df", + "metadata": {}, + "source": [ + "### Assign quality models to targets\n", + "\n", + "`QualityTargets` extends `CityTargets`: it generates city-based targets through the parent class and then attaches a quality weight, a selected quality model, and that model's randomized parameters to each target. A scalar weight applies to all targets; a two-element range samples a separate weight for each target. A one-element tuple of quality names selects the same model for every target.\n", + "\n", + "On each reset, the scenario initializes the best obtained reward to `0.0` and clears the per-satellite capture timestamps. These fields support marginal rewards for re-imaging and the cooldown filter used in the coordination example below.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "hybrid-quality-03", + "metadata": {}, + "outputs": [], + "source": [ + "from copy import deepcopy\n", + "\n", + "\n", + "class QualityTargets(CityTargets):\n", + " \"\"\"City-based target scenario with per-target quality models and weights.\"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " n_targets: int | tuple[int, int],\n", + " n_select_from: int | None = None,\n", + " location_offset: float = 0,\n", + " priority_distribution: Callable | None = None,\n", + " radius: float = orbitalMotion.REQ_EARTH * 1e3,\n", + " quality_weights: float | list[float] = 0.5,\n", + " quality_function_name: list[str] | None = None,\n", + " quality_function_weights: None | list[float] = None,\n", + " ) -> None:\n", + "\n", + " super().__init__(\n", + " n_targets, n_select_from, location_offset, priority_distribution, radius\n", + " )\n", + " self.quality_weights = quality_weights\n", + " if quality_function_name is None:\n", + " self.quality_function_name = [\n", + " \"quality_off_nadir_angle\",\n", + " \"quality_periodic\",\n", + " ]\n", + " self.quality_function_name = deepcopy(quality_function_name)\n", + " self.quality_function_weights = quality_function_weights\n", + " if self.quality_function_weights is not None:\n", + " assert len(self.quality_function_name) == len(quality_function_weights), (\n", + " \"quality_function_weights must match quality_functions\"\n", + " )\n", + " self.quality_function_weights = np.array(quality_function_weights) / np.sum(\n", + " np.array(quality_function_weights)\n", + " )\n", + "\n", + " def regenerate_targets(self) -> None:\n", + " super().regenerate_targets()\n", + "\n", + " for tgt in self.targets:\n", + " if isinstance(self.quality_weights, (list, tuple, np.ndarray)):\n", + " quality_weight = np.random.uniform(\n", + " self.quality_weights[0], self.quality_weights[1]\n", + " )\n", + " else:\n", + " quality_weight = self.quality_weights\n", + "\n", + " quality_function = np.random.choice(\n", + " self.quality_function_name, p=self.quality_function_weights\n", + " )\n", + "\n", + " quality_function_params = get_randomized_quality_function_params(\n", + " quality_function\n", + " )\n", + "\n", + " tgt.quality_weight = quality_weight\n", + " tgt.quality_function_name = quality_function\n", + " tgt.quality_function_params = quality_function_params\n", + " tgt.highest_quality_obtained = None\n", + " tgt.highest_quality_time = None\n", + " tgt.highest_reward_obtained = 0.0\n", + " tgt.last_img_time_per_sat = {}" + ] + }, + { + "cell_type": "markdown", + "id": "hybrid-quality-04", + "metadata": {}, + "source": [ + "## Rewarding physical captures\n", + "\n", + "`QualityImageStore` detects a completed acquisition from an increase in stored instrument data and evaluates quality using the target's assigned model and the satellite state at capture time. Requested acquisition time and realized capture time are recorded separately because pointing constraints can delay capture.\n", + "\n", + "`QualityImageReward` converts these acquisition events into step rewards. With `incremental_score=False`, a target is rewarded only for its first completed acquisition. With `incremental_score=True`, a capture earns $\\max(r_i(t)-r_{i,\\mathrm{max}},0)$, where $r_{i,\\mathrm{max}}$ is the best absolute acquisition reward completed before that capture. Pending intents affect predicted marginal profiles, but do not increase the completed-reward baseline used for actual reward allocation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "hybrid-quality-05", + "metadata": {}, + "outputs": [], + "source": [ + "class QualityImageData(UniqueImageData):\n", + " def __init__(self, imaged=None, duplicates=0, known=None, image_events=None):\n", + " super().__init__(imaged=imaged, duplicates=duplicates, known=known)\n", + "\n", + " if image_events is None:\n", + " image_events = []\n", + " self.image_events = [event.copy() for event in image_events]\n", + "\n", + " def __add__(self, other: \"QualityImageData\") -> \"QualityImageData\":\n", + " imaged = self.imaged | other.imaged\n", + " image_events = self.image_events + other.image_events\n", + " duplicates = (\n", + " self.duplicates\n", + " + other.duplicates\n", + " + len(self.imaged)\n", + " + len(other.imaged)\n", + " - len(imaged)\n", + " )\n", + " known = self.known | other.known\n", + " return self.__class__(\n", + " imaged=imaged,\n", + " duplicates=duplicates,\n", + " known=known,\n", + " image_events=image_events,\n", + " )\n", + "\n", + "\n", + "def get_quality(sat, tgt, t=None, **function_context):\n", + " \"\"\"Evaluate the quality function assigned to a target.\"\"\"\n", + " if t is None:\n", + " t = sat.simulator.sim_time\n", + "\n", + " quality_function = tgt.quality_function_name\n", + " quality_function_params = {\n", + " **tgt.quality_function_params,\n", + " **function_context,\n", + " }\n", + " return QUALITY_FUNCTIONS[quality_function](\n", + " sat,\n", + " tgt,\n", + " t=t,\n", + " **quality_function_params,\n", + " )\n", + "\n", + "\n", + "class QualityImageStore(UniqueImageStore):\n", + " \"\"\"Record quality-aware target acquisitions, including repeated captures.\"\"\"\n", + "\n", + " data_type = QualityImageData\n", + "\n", + " def __init__(\n", + " self,\n", + " *args,\n", + " record_image_profiles: bool = False,\n", + " image_profile_samples: int = 25,\n", + " **kwargs,\n", + " ) -> None:\n", + " \"\"\"Initialize image-quality logging options.\n", + "\n", + " Args:\n", + " record_image_profiles: Sample quality over the acquisition access window.\n", + " This is intended for post-test visualization and is disabled by default\n", + " to avoid adding work to training environments.\n", + " image_profile_samples: Number of evenly spaced samples in each profile.\n", + " \"\"\"\n", + " if image_profile_samples < 2:\n", + " raise ValueError(\"image_profile_samples must be at least 2\")\n", + " self.record_image_profiles = record_image_profiles\n", + " self.image_profile_samples = image_profile_samples\n", + " super().__init__(*args, **kwargs)\n", + "\n", + " def _access_window(self, target: \"Target\", time: float) -> tuple[float, float]:\n", + " \"\"\"Return a containing window, otherwise the nearest known window or NaNs.\"\"\"\n", + " candidates = []\n", + " for opportunity in getattr(self.satellite, \"opportunities\", []):\n", + " if opportunity.get(\"type\") != \"target\":\n", + " continue\n", + " opportunity_target = opportunity.get(\"object\")\n", + " if opportunity_target is not target and getattr(\n", + " opportunity_target, \"id\", None\n", + " ) != getattr(target, \"id\", None):\n", + " continue\n", + " window = opportunity.get(\"window\")\n", + " if window is None or len(window) != 2:\n", + " continue\n", + " window_open, window_close = map(float, window)\n", + " if window_open <= time <= window_close:\n", + " return window_open, window_close\n", + " candidates.append((window_open, window_close))\n", + "\n", + " if candidates:\n", + " return min(\n", + " candidates,\n", + " key=lambda window: min(\n", + " abs(time - window[0]),\n", + " abs(time - window[1]),\n", + " ),\n", + " )\n", + " return np.nan, np.nan\n", + "\n", + " def _requested_imaging_time(self) -> float:\n", + " \"\"\"Read the controller's absolute requested imaging time, when available.\"\"\"\n", + " try:\n", + " return float(self.satellite.fsw.insControl.imagingTime) * 1e-9\n", + " except (AttributeError, TypeError, ValueError):\n", + " return np.nan\n", + "\n", + " def _quality_profile(\n", + " self,\n", + " target: \"Target\",\n", + " window_open: float,\n", + " window_close: float,\n", + " ) -> tuple[np.ndarray, np.ndarray]:\n", + " \"\"\"Sample raw target quality over an access window.\"\"\"\n", + " if (\n", + " not self.record_image_profiles\n", + " or not np.all(np.isfinite([window_open, window_close]))\n", + " or window_close <= window_open\n", + " or not all(\n", + " hasattr(target, attribute)\n", + " for attribute in (\n", + " \"quality_function_name\",\n", + " \"quality_function_params\",\n", + " )\n", + " )\n", + " ):\n", + " return np.asarray([], dtype=float), np.asarray([], dtype=float)\n", + "\n", + " profile_times = np.linspace(\n", + " window_open,\n", + " window_close,\n", + " self.image_profile_samples,\n", + " )\n", + "\n", + " profile_qualities = np.asarray(\n", + " get_quality(\n", + " self.satellite,\n", + " target,\n", + " t=profile_times,\n", + " w_o=window_open,\n", + " w_c=window_close,\n", + " ),\n", + " dtype=float,\n", + " )\n", + "\n", + " if profile_qualities.shape != profile_times.shape or not np.all(\n", + " np.isfinite(profile_qualities)\n", + " ):\n", + " return np.asarray([], dtype=float), np.asarray([], dtype=float)\n", + " return profile_times, np.clip(profile_qualities, 0.0, 1.0)\n", + "\n", + " def get_log_state(self) -> float:\n", + " \"\"\"Log the instantaneous storage unit state at the end of each step.\n", + " Returns:\n", + " float: storedData from satellite storage unit\n", + " \"\"\"\n", + " msg = self.satellite.dynamics.storageUnit.storageUnitDataOutMsg.read()\n", + "\n", + " return msg.storedData[0]\n", + "\n", + " def compare_log_states(\n", + " self, old_state: float, new_state: float\n", + " ) -> QualityImageData:\n", + " \"\"\"Record an acquisition when stored instrument data increases.\n", + "\n", + " Args:\n", + " old_state: Previous scalar stored-data reading.\n", + " new_state: Current scalar stored-data reading.\n", + "\n", + " Returns:\n", + " QualityImageData containing the captured target and acquisition event,\n", + " or an empty record when storage does not increase. Repeated captures\n", + " of the same target are recorded as separate events.\n", + " \"\"\"\n", + " data_increase = new_state - old_state\n", + "\n", + " if data_increase <= 0:\n", + " return QualityImageData()\n", + " else:\n", + " assert self.satellite.latest_target is not None\n", + " target = self.satellite.latest_target\n", + " self.update_target_colors([target])\n", + " image_time = float(self.satellite.simulator.sim_time)\n", + " window_open, window_close = self._access_window(target, image_time)\n", + "\n", + " quality = get_quality(\n", + " self.satellite,\n", + " target,\n", + " t=image_time,\n", + " w_o=window_open,\n", + " w_c=window_close,\n", + " )\n", + "\n", + " target.last_quality_obtained = quality\n", + " target.last_quality_time = image_time\n", + " if (\n", + " target.highest_quality_obtained is None\n", + " or quality > target.highest_quality_obtained\n", + " ):\n", + " target.highest_quality_obtained = quality\n", + " target.highest_quality_time = image_time\n", + "\n", + " target.last_img_time_per_sat[self.satellite.name] = image_time\n", + "\n", + " profile_times, profile_qualities = self._quality_profile(\n", + " target,\n", + " window_open,\n", + " window_close,\n", + " )\n", + " image_event = {\n", + " \"time\": image_time,\n", + " \"requested_time\": self._requested_imaging_time(),\n", + " \"target_id\": str(target.id),\n", + " \"target_name\": str(target.name),\n", + " \"satellite_name\": str(self.satellite.name),\n", + " \"priority\": float(target.priority),\n", + " \"quality\": float(quality),\n", + " \"quality_function_name\": str(\n", + " getattr(target, \"quality_function_name\", \"legacy_distance\")\n", + " ),\n", + " \"quality_function_params\": dict(\n", + " getattr(target, \"quality_function_params\", {})\n", + " ),\n", + " \"access_window_open\": window_open,\n", + " \"access_window_close\": window_close,\n", + " \"imaging_time_fraction\": (image_time - window_open)\n", + " / (window_close - window_open),\n", + " \"profile_times\": profile_times,\n", + " \"profile_qualities\": profile_qualities,\n", + " \"quality_weight\": np.nan,\n", + " \"reward\": 0.0,\n", + " \"rewarded\": False,\n", + " }\n", + " return QualityImageData(\n", + " imaged={target},\n", + " image_events=[image_event],\n", + " )\n", + "\n", + "\n", + "class QualityImageReward(UniqueImageReward):\n", + " \"\"\"Reward initial captures or positive re-imaging improvements.\"\"\"\n", + "\n", + " data_store_type = QualityImageStore\n", + "\n", + " def __init__(\n", + " self,\n", + " reward_fn: Callable,\n", + " record_image_profiles: bool = False,\n", + " image_profile_samples: int = 25,\n", + " incremental_score: bool = False,\n", + " ):\n", + " \"\"\"Initialize the rewarder.\n", + "\n", + " Args:\n", + " reward_fn: Callback evaluated as\n", + " reward_fn(priority, quality, quality_weight=target.quality_weight).\n", + " The quality weight is supplied by each target.\n", + " record_image_profiles: Sample target quality over each successful image's\n", + " access window for post-test visualization.\n", + " image_profile_samples: Number of samples stored in each quality profile.\n", + " incremental_score: If True, reward only positive improvements over\n", + " the best completed reward for each target. If False, reward only\n", + " the first completed acquisition of each target.\n", + " \"\"\"\n", + "\n", + " super().__init__(reward_fn)\n", + " self.data_store_kwargs.update(\n", + " record_image_profiles=record_image_profiles,\n", + " image_profile_samples=image_profile_samples,\n", + " )\n", + " self.incremental_score = incremental_score\n", + "\n", + " def create_data_store(self, satellite) -> None:\n", + " \"\"\"Create the store, bypassing the unique-target filter for re-imaging.\"\"\"\n", + " if self.incremental_score:\n", + " GlobalReward.create_data_store(self, satellite)\n", + " else:\n", + " super().create_data_store(satellite)\n", + "\n", + " def _update_event_reward_info(\n", + " self,\n", + " event: dict[str, Any],\n", + " target: \"Target\",\n", + " quality_weight: float,\n", + " reward: float,\n", + " rewarded: bool,\n", + " ) -> None:\n", + " \"\"\"Attach reward information to an acquisition event in place.\"\"\"\n", + " event[\"quality_weight\"] = float(quality_weight)\n", + " event[\"reward\"] = float(reward)\n", + " event[\"rewarded\"] = rewarded\n", + "\n", + " profile_qualities = np.asarray(event[\"profile_qualities\"], dtype=float)\n", + " if profile_qualities.size == 0:\n", + " event[\"profile_rewards\"] = np.asarray([], dtype=float)\n", + " return\n", + "\n", + " try:\n", + " profile_rewards = np.asarray(\n", + " self.reward_fn(\n", + " target.priority,\n", + " profile_qualities,\n", + " quality_weight=quality_weight,\n", + " ),\n", + " dtype=float,\n", + " )\n", + " if profile_rewards.ndim == 0:\n", + " profile_rewards = np.full_like(profile_qualities, profile_rewards)\n", + " except (TypeError, ValueError):\n", + " profile_rewards = np.asarray(\n", + " [\n", + " self.reward_fn(\n", + " target.priority,\n", + " quality,\n", + " quality_weight=quality_weight,\n", + " )\n", + " for quality in profile_qualities\n", + " ],\n", + " dtype=float,\n", + " )\n", + " event[\"profile_rewards\"] = profile_rewards\n", + "\n", + " def calculate_reward(\n", + " self, new_data_dict: dict[str, QualityImageData]\n", + " ) -> dict[str, float]:\n", + " \"\"\"Allocate step rewards for completed acquisitions.\n", + "\n", + " Evaluate reward_fn(priority, quality, quality_weight=target.quality_weight).\n", + " Nonincremental scoring rewards only the first acquisition of a target;\n", + " incremental scoring rewards positive improvements over its best completed\n", + " reward. For simultaneous captures, allocate reward among satellites tied\n", + " for the highest realized quality and give the others zero.\n", + "\n", + " Args:\n", + " new_data_dict: Acquisition data keyed by satellite name.\n", + "\n", + " Returns:\n", + " Dictionary mapping each satellite name to its total reward this step.\n", + " \"\"\"\n", + "\n", + " event_per_target = {}\n", + " targets_by_id = {}\n", + " for sat_id, new_data in new_data_dict.items():\n", + " targets_by_id.update({str(target.id): target for target in new_data.imaged})\n", + " for event in new_data.image_events:\n", + " target_id = event[\"target_id\"]\n", + " if target_id not in event_per_target:\n", + " event_per_target[target_id] = []\n", + " event_per_target[target_id].append((sat_id, event))\n", + "\n", + " reward = {sat_id: 0.0 for sat_id in new_data_dict}\n", + " for tgt_id, events in event_per_target.items():\n", + " target = targets_by_id.get(tgt_id)\n", + "\n", + " if target in self.data.imaged and not self.incremental_score:\n", + " for sat_id, event in events:\n", + " self._update_event_reward_info(\n", + " event,\n", + " target,\n", + " target.quality_weight,\n", + " reward=0.0,\n", + " rewarded=False,\n", + " )\n", + " continue\n", + "\n", + " time_list = np.array([event[\"time\"] for _, event in events])\n", + " time_events = set(time_list)\n", + " time_events_sorted = sorted(time_events)\n", + "\n", + " for time_event in time_events_sorted:\n", + " events_at_time = [\n", + " (sat_id, event)\n", + " for sat_id, event in events\n", + " if event[\"time\"] == time_event\n", + " ]\n", + "\n", + " max_quality = max(event[\"quality\"] for _, event in events_at_time)\n", + " sat_ids_max_quality = [\n", + " sat_id\n", + " for sat_id, event in events_at_time\n", + " if event[\"quality\"] == max_quality\n", + " ]\n", + "\n", + " scaling_conflict_score = {\n", + " sat_id: (\n", + " 1.0 / len(sat_ids_max_quality)\n", + " if sat_id in sat_ids_max_quality\n", + " else 0.0\n", + " )\n", + " for sat_id, _ in events_at_time\n", + " }\n", + "\n", + " if self.incremental_score:\n", + " if target.highest_reward_obtained is None:\n", + " highest_reward_obtained = 0\n", + " else:\n", + " highest_reward_obtained = target.highest_reward_obtained\n", + "\n", + " highest_new_reward = None\n", + " for sat_id, event in events_at_time:\n", + " quality = event.get(\"quality\", 0.0)\n", + "\n", + " step_reward = self.reward_fn(\n", + " target.priority, quality, quality_weight=target.quality_weight\n", + " )\n", + "\n", + " if self.incremental_score:\n", + " if (\n", + " highest_new_reward is None\n", + " or step_reward > highest_new_reward\n", + " ):\n", + " highest_new_reward = step_reward\n", + " step_reward = max(0.0, step_reward - highest_reward_obtained)\n", + "\n", + " step_reward = step_reward * scaling_conflict_score.get(sat_id, 0.0)\n", + "\n", + " reward[sat_id] += step_reward\n", + "\n", + " self._update_event_reward_info(\n", + " event,\n", + " target,\n", + " target.quality_weight,\n", + " step_reward,\n", + " rewarded=step_reward > 0.0,\n", + " )\n", + "\n", + " if (\n", + " self.incremental_score\n", + " and highest_new_reward > highest_reward_obtained\n", + " ):\n", + " target.highest_reward_obtained = highest_new_reward\n", + " target.highest_reward_time = time_event\n", + "\n", + " return reward" + ] + }, + { + "cell_type": "markdown", + "id": "hybrid-quality-06", + "metadata": {}, + "source": [ + "## Satellite and observations\n", + "\n", + "Following the paper's observation representation, a quadratic B-spline approximates the estimated marginal reward over each remaining observation window. Its five coefficients are passed to the agent.\n", + "\n", + "The observation has 32 satellite/context features (12 spacecraft properties and 20 priority-density bins), followed by 40 target tokens with 12 features each: five reward-spline coefficients, relative position (3), pointing angle, angular rate, and opening and closing offsets. Both the spline and action wrapper parameterize the **remaining** window, using the current time as the start when the window is already open. The action wrapper enforces a minimum delay of 0.5 seconds. Opening offsets in the observation remain signed and can therefore be negative for an already-open window.\n", + "\n", + "`PriorityDensity` is a custom observation class that summarizes future imaging opportunities in 20 consecutive 180-second bins. Each target contributes its priority, divided by five, to the bin containing the closing time of its next available window. This provides context about upcoming opportunities beyond the 40 targets represented individually.\n", + "\n", + "`TargetBookkeeping` stores the best completed reward known for each target and, when the communication mode uses them, competing intended rewards. Its combined baseline is the larger of the completed and intended values. An intent changes the expected marginal benefit of an acquisition; it does not count as a completed image or increase the obtained reward.\n", + "\n", + "`estimate_quality_profile` samples absolute predicted reward at 25 times over the remaining window, subtracts the satellite's current bookkeeping baseline, and clips the result to [0, 1] before fitting the spline. With empty bookkeeping, the baseline is zero. Absolute predictions are cached, but the baseline is read on every call. The coordination loop updates bookkeeping and clears the observation cache before requesting a new observation, so the spline values change while the observation dimensions remain fixed." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "hybrid-quality-07", + "metadata": {}, + "outputs": [], + "source": [ + "class PriorityDensity(obs.Observation):\n", + " \"\"\"Priority sums in twenty future 180-second opportunity-closing bins.\"\"\"\n", + "\n", + " def __init__(self):\n", + " super().__init__(name=\"priority_density\")\n", + "\n", + " def get_obs(self):\n", + " now = self.simulator.sim_time\n", + " self.satellite.calculate_additional_windows(\n", + " now + 21 * 180 - self.satellite.window_calculation_time\n", + " )\n", + " upcoming = self.satellite.upcoming_opportunities_dict(types=\"target\")\n", + " density = np.zeros(20)\n", + " for target, windows in upcoming.items():\n", + " index = int(np.floor((windows[0][1] - now) / 180))\n", + " if 0 <= index < len(density):\n", + " density[index] += target.priority / 5.0\n", + " return density\n", + "\n", + "\n", + "class TargetBookkeeping:\n", + " def __init__(self):\n", + " self._reset()\n", + "\n", + " def _reset(self):\n", + " self.bookkeeping = {}\n", + " self.bookkeeping_imaged = {}\n", + " self.bookkeeping_intended = {}\n", + "\n", + " def update(\n", + " self,\n", + " tgt_id: str,\n", + " obtained_reward: float,\n", + " time: float,\n", + " ) -> None:\n", + " if tgt_id not in self.bookkeeping_imaged:\n", + " self.bookkeeping_imaged[tgt_id] = {\"reward\": obtained_reward, \"time\": time}\n", + "\n", + " else:\n", + " if obtained_reward > self.bookkeeping_imaged[tgt_id][\"reward\"]:\n", + " self.bookkeeping_imaged[tgt_id] = {\n", + " \"reward\": obtained_reward,\n", + " \"time\": time,\n", + " }\n", + "\n", + " self._update()\n", + "\n", + " def update_intended(\n", + " self,\n", + " tgt_id: str,\n", + " expected_reward: float,\n", + " time: float,\n", + " ) -> None:\n", + "\n", + " if (\n", + " tgt_id in self.bookkeeping_imaged\n", + " and expected_reward <= self.bookkeeping_imaged[tgt_id][\"reward\"]\n", + " ):\n", + " return\n", + "\n", + " if tgt_id not in self.bookkeeping_intended:\n", + " self.bookkeeping_intended[tgt_id] = {\n", + " \"reward\": expected_reward,\n", + " \"time\": time,\n", + " }\n", + "\n", + " else:\n", + " if expected_reward > self.bookkeeping_intended[tgt_id][\"reward\"]:\n", + " self.bookkeeping_intended[tgt_id] = {\n", + " \"reward\": expected_reward,\n", + " \"time\": time,\n", + " }\n", + "\n", + " self._update()\n", + "\n", + " def delete_intended(self):\n", + " self.bookkeeping_intended = {}\n", + " self._update()\n", + "\n", + " def _update(self):\n", + " self.bookkeeping = copy(self.bookkeeping_imaged)\n", + " tgt_ids_imaged = set(self.bookkeeping_imaged.keys())\n", + " tgt_ids_intended = set(self.bookkeeping_intended.keys())\n", + " for tgt_id in tgt_ids_intended:\n", + " if tgt_id not in tgt_ids_imaged:\n", + " self.bookkeeping[tgt_id] = self.bookkeeping_intended[tgt_id]\n", + " else:\n", + " if (\n", + " self.bookkeeping_intended[tgt_id][\"reward\"]\n", + " > self.bookkeeping_imaged[tgt_id][\"reward\"]\n", + " ):\n", + " self.bookkeeping[tgt_id] = self.bookkeeping_intended[tgt_id]\n", + "\n", + " def get_reward(self, tgt_id: str) -> float:\n", + " if tgt_id not in self.bookkeeping:\n", + " return 0.0\n", + " else:\n", + " return self.bookkeeping[tgt_id][\"reward\"]\n", + "\n", + "\n", + "def _quality_profile_context(sat):\n", + " \"\"\"Cache trajectory data shared by all quality profiles at one simulation step.\"\"\"\n", + " trajectory = sat.trajectory\n", + " cache_key = (\n", + " id(trajectory),\n", + " sat.simulator.sim_time,\n", + " trajectory.sim_time,\n", + " len(trajectory.times),\n", + " )\n", + " cache = getattr(sat, \"_quality_profile_context\", None)\n", + "\n", + " if cache is None or cache[\"key\"] != cache_key:\n", + " cache = {\n", + " \"key\": cache_key,\n", + " \"trajectory_r_BP_P\": trajectory.r_BP_P,\n", + " \"full_reward_profiles\": {},\n", + " }\n", + " sat._quality_profile_context = cache\n", + "\n", + " return cache\n", + "\n", + "\n", + "def estimate_absolute_reward(sat, tgt, times):\n", + " \"\"\"Predict acquisition reward without completed or intended reward baselines.\n", + "\n", + " ``times`` contains absolute simulator times. Use the same trajectory and\n", + " quality assumptions as the observation profiles, but do not infer reward\n", + " from their clipped marginal splines.\n", + " \"\"\"\n", + " profile_context = _quality_profile_context(sat)\n", + " raw_quality = QUALITY_FUNCTIONS[tgt.quality_function_name](\n", + " sat,\n", + " tgt,\n", + " np.asarray(times, dtype=float),\n", + " **tgt.quality_function_params,\n", + " trajectory_r_BP_P=profile_context[\"trajectory_r_BP_P\"],\n", + " )\n", + " return np.asarray(\n", + " reward_function(tgt.priority, raw_quality, quality_weight=tgt.quality_weight),\n", + " dtype=float,\n", + " )\n", + "\n", + "\n", + "def estimate_quality_profile(sat, opp, return_rewards: bool = False):\n", + " tgt = opp[\"object\"]\n", + " window = opp[\"window\"]\n", + " window_open = np.max([window[0], sat.simulator.sim_time])\n", + " window_close = window[1]\n", + " profile_context = _quality_profile_context(sat)\n", + "\n", + " profile_key = (\n", + " str(tgt.id),\n", + " float(window_open),\n", + " float(window_close),\n", + " )\n", + " profile = profile_context[\"full_reward_profiles\"].get(profile_key)\n", + "\n", + " if profile is None:\n", + " times = np.linspace(window_open, window_close, 25)\n", + " full_reward = estimate_absolute_reward(sat, tgt, times)\n", + " profile = {\n", + " \"times\": times,\n", + " \"full_reward\": full_reward,\n", + " }\n", + " profile_context[\"full_reward_profiles\"][profile_key] = profile\n", + "\n", + " obtained_reward = (\n", + " sat.bookkeeping.get_reward(tgt.id) if hasattr(sat, \"bookkeeping\") else 0.0\n", + " )\n", + "\n", + " marginal_reward = np.clip(\n", + " profile[\"full_reward\"] - obtained_reward,\n", + " a_min=0.0,\n", + " a_max=1.0,\n", + " )\n", + "\n", + " if return_rewards:\n", + " return marginal_reward\n", + "\n", + " # Normalize sample times to [0, 1] over the remaining access window.\n", + " x_vector = (profile[\"times\"] - window_open) / (window_close - window_open)\n", + "\n", + " spline = make_lsq_spline(x_vector, marginal_reward, t=SPLINE_KNOTS, k=SPLINE_DEGREE)\n", + "\n", + " return spline.c" + ] + }, + { + "cell_type": "markdown", + "id": "22e1ea2d", + "metadata": {}, + "source": [ + "### Define the satellite\n", + "\n", + "`QualitySatellite` combines the spacecraft dynamics and flight software with the observations defined above. Its observation specification includes spacecraft properties, `PriorityDensity`, and the next `N_AHEAD` target opportunities. The opportunity callback uses `estimate_quality_profile` to supply the five reward-spline coefficients alongside each target's geometry and window offsets. Each satellite has its own bookkeeping, which is cleared when the environment resets.\n", + "\n", + "The action specification uses `act.Image(n_ahead_image=N_AHEAD, hybrid=True)`. This enables a native imaging action containing a target-opportunity index and a desired acquisition delay in seconds. The wrappers below expose normalized timing fractions and, optionally, a categorical action over target/timing pairs. `satellite_args` supplies the orbit, pointing constraints, timing tolerances, and spacecraft resource settings shared by training and evaluation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "6e515629", + "metadata": {}, + "outputs": [], + "source": [ + "from functools import partial\n", + "\n", + "\n", + "class QualitySatellite(sats.ImagingSatellite):\n", + " dyn_type = dyn.FullFeaturedDynModel\n", + " fsw_type = fsw.SteeringImagerFSWModel\n", + " observation_spec: ClassVar = [\n", + " obs.SatProperties(\n", + " dict(prop=\"omega_BN_B\", norm=0.03),\n", + " dict(prop=\"c_hat_H\"),\n", + " dict(prop=\"r_BN_P\", norm=EARTH_RADIUS),\n", + " dict(prop=\"v_BN_P\", norm=7616.5),\n", + " ),\n", + " PriorityDensity(),\n", + " obs.OpportunityProperties(\n", + " dict(fn=partial(estimate_quality_profile)),\n", + " dict(prop=\"r_LB_H\", norm=EARTH_RADIUS),\n", + " dict(prop=\"target_angle\", norm=np.pi / 2),\n", + " dict(prop=\"target_angle_rate\", norm=0.03),\n", + " dict(prop=\"opportunity_open\", norm=300),\n", + " dict(prop=\"opportunity_close\", norm=300),\n", + " n_ahead_observe=N_AHEAD,\n", + " ),\n", + " ]\n", + " action_spec: ClassVar = [act.Image(n_ahead_image=N_AHEAD, hybrid=True)]\n", + "\n", + " def __init__(self, *args, **kwargs):\n", + " super().__init__(*args, **kwargs)\n", + " self.bookkeeping = TargetBookkeeping()\n", + "\n", + " def reset_pre_sim_init(self) -> None:\n", + " super().reset_pre_sim_init()\n", + " self.bookkeeping._reset()\n", + "\n", + "\n", + "def satellite_args():\n", + " return QualitySatellite.default_sat_args(\n", + " oe=lambda: random_circular_orbit(alt=ALTITUDE_KM, i=45),\n", + " imageAttErrorRequirement=0.01,\n", + " imageRateErrorRequirement=0.01,\n", + " imageTargetMinimumElevation=np.arctan(800 / 500),\n", + " timeToleranceLower=1.0, # Seconds; BSK-RL converts to nanoseconds.\n", + " timeToleranceUpper=0.0, # Zero disables the upper bound, allowing late capture.\n", + " u_max=0.4,\n", + " K1=0.25,\n", + " K3=3.0,\n", + " servo_P=30,\n", + " omega_max=np.radians(5),\n", + " rwBasePower=20,\n", + " disturbance_vector=np.zeros(3),\n", + " batteryStorageCapacity=80 * 3600 * 2 * 1000,\n", + " storedCharge_Init=0.99 * 80 * 3600 * 2 * 1000,\n", + " maxWheelSpeed=15000,\n", + " wheelSpeeds=lambda: np.random.uniform(-1, 1, 3),\n", + " dataStorageCapacity=2 * 8e6 * 25 * 1000,\n", + " storageInit=0,\n", + " transmitterPacketSize=0,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "hybrid-quality-08", + "metadata": {}, + "source": [ + "## Minimal wrappers and environment factory\n", + "\n", + "With `hybrid=True` and at least one imaging time tolerance enabled, the native imaging action accepts a target index and a desired acquisition delay in seconds. This delay specifies when to try to acquire the image; it is not the exposure duration. If both tolerances are zero, the continuous value instead specifies the maximum imaging action duration in seconds, capped by `max_duration` and with a minimum of one simulator time step. The wrappers let a policy select a target and a normalized position within its remaining observation window.\n", + "\n", + "### From a discrete option to a target and timing fraction\n", + "\n", + "Let $N$ be the number of upcoming target opportunities and let $F=(F_0,\\ldots,F_{D-1})$ contain $D$ predefined timing fractions. The policy selects one categorical action $a\\in\\{0,\\ldots,ND-1\\}$. `WrapperDiscreteToHybridAction` decodes it as\n", + "\n", + "$$\n", + "i=\\left\\lfloor\\frac{a}{D}\\right\\rfloor,\\qquad\n", + "k=a\\bmod D,\\qquad f=F_k.\n", + "$$\n", + "\n", + "Here $i$ is the zero-based target-opportunity index and $k$ is the timing-option index. The inverse mapping is $a=iD+k$. Timing options vary fastest within each target. With `N_AHEAD=40` and `IMAGING_FRACTIONS=(0.0, 0.5, 1.0)`, each satellite has `Discrete(120)`:\n", + "\n", + "| Discrete action | Target index | Timing fraction |\n", + "|---|---|---|\n", + "| 6 | 2 | 0.0 |\n", + "| 7 | 2 | 0.5 |\n", + "| 8 | 2 | 1.0 |\n", + "\n", + "The actor therefore needs a categorical distribution over 120 choices. Although these choices are translated into real-valued delays, the policy only chooses among the three listed fractions; it does not output an arbitrary continuous timing value.\n", + "\n", + "### From a timing fraction to seconds\n", + "\n", + "For current simulation time $t_{\\mathrm{now}}$ and the selected opportunity's physical window $[t_{\\mathrm{open}},t_{\\mathrm{close}}]$, define the remaining opening and closing offsets\n", + "\n", + "$$\n", + "u=\\max(0,t_{\\mathrm{open}}-t_{\\mathrm{now}}),\\qquad\n", + "v=t_{\\mathrm{close}}-t_{\\mathrm{now}}.\n", + "$$\n", + "\n", + "`WrapperActionDuration` clips $f$ to $[0,1]$ and returns\n", + "\n", + "$$\n", + "\\Delta t_{\\mathrm{img}}=\\max\\bigl(0.5,\\;u+f(v-u)\\bigr),\\qquad\n", + "t_d=t_{\\mathrm{now}}+\\Delta t_{\\mathrm{img}}.\n", + "$$\n", + "\n", + "The native action receives `(i, delta_t_img)` and the controller uses the absolute desired acquisition time $t_d$. For example, if the current time is 100 seconds and target opportunity 2 spans [120, 180] seconds, action 7 selects the midpoint: a 50-second delay and a desired acquisition time of 150 seconds. If the window instead spans [90, 140] seconds, the same fraction selects the midpoint of the remaining window: a 20-second delay and a desired acquisition time of 120 seconds. Fraction zero requests the earliest remaining time, subject to the 0.5-second minimum delay.\n", + "\n", + "The wrapper stack processes actions in this order:\n", + "\n", + "```text\n", + "discrete action a\n", + " -> WrapperDiscreteToHybridAction: (target index i, fraction f)\n", + " -> WrapperActionDuration: (target index i, delay in seconds)\n", + " -> WrapperActionLogging\n", + " -> native imaging action: absolute desired time = now + delay\n", + "```\n", + "\n", + "For a constellation, this conversion is applied separately to each supplied entry in the satellite-name action dictionary.\n", + "\n", + "### Acquisition tolerances and action limits\n", + "\n", + "Acquisition also requires the pointing-angle and angular-rate constraints to be satisfied. When both timing bounds are enabled, the allowed acquisition times satisfy\n", + "\n", + "$$\n", + "t_d-\\tau_{\\mathrm{lower}}\\leq t\\leq t_d+\\tau_{\\mathrm{upper}},\n", + "$$\n", + "\n", + "where the positive tolerances are configured in seconds through `timeToleranceLower` and `timeToleranceUpper`. A zero tolerance disables its corresponding bound independently. The current settings, `timeToleranceLower=1.0` and `timeToleranceUpper=0.0`, allow acquisition from one second before $t_d$ onward, without an instrument upper timing bound. A late acquisition is therefore possible if the pointing requirements are met later. Setting both tolerances to zero removes both timing gates and makes the continuous action value a maximum imaging duration. The action can finish sooner if an image is captured. The mode is selected once per environment reset from the initialized controller tolerances, which must remain fixed during an episode; enabling either bound emits a warning explaining the capture-time interpretation.\n", + "\n", + "Physical access, action-duration limits, and simulation stepping still apply. A positive upper tolerance also lets the native imaging action limit its timeout using the requested delay plus that tolerance. With the upper tolerance disabled, this example retains the physical window and existing action/environment limits; the fraction does not impose a new upper deadline.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "hybrid-quality-09", + "metadata": {}, + "outputs": [], + "source": [ + "from bsk_rl.gym import is_no_action\n", + "\n", + "\n", + "def clean_action(\n", + " action: tuple[int, float | np.ndarray | list],\n", + ") -> tuple[int, float]:\n", + " if action is None:\n", + " return None\n", + " discrete = action[0].item() if isinstance(action[0], np.generic) else action[0]\n", + " val = action[1][0] if isinstance(action[1], (list, np.ndarray)) else action[1]\n", + " continuous = val.item() if isinstance(val, np.generic) else val\n", + "\n", + " return (discrete, continuous)\n", + "\n", + "\n", + "class WrapperActionDuration(gym.ActionWrapper):\n", + " \"\"\"Convert a fraction into an acquisition delay over the remaining window.\n", + "\n", + " The returned delay is in seconds and has a minimum of 0.5 seconds.\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " env,\n", + " ) -> None:\n", + " super().__init__(env)\n", + " self.env = env\n", + "\n", + " self._n_ahead = {}\n", + " for sat in self.env.satellites:\n", + " obs_class = next(\n", + " item\n", + " for item in sat.observation_spec\n", + " if isinstance(item, (obs.OpportunityProperties))\n", + " and item.type == \"target\"\n", + " )\n", + " self._n_ahead[sat.name] = obs_class.n_ahead_observe\n", + "\n", + " def get_image_window(\n", + " self, target_index: int, sat_name: str | None\n", + " ) -> tuple[float, float]:\n", + "\n", + " if sat_name is not None:\n", + " sat = next(sat for sat in self.unwrapped.satellites if sat.name == sat_name)\n", + " else:\n", + " sat = self.unwrapped.satellite\n", + "\n", + " opps = sat.find_next_opportunities(\n", + " n=self._n_ahead[sat.name],\n", + " types=\"target\",\n", + " pad=True,\n", + " )\n", + "\n", + " if isinstance(target_index, (int, np.integer)):\n", + " opp = opps[target_index]\n", + " else:\n", + " opp = [\n", + " tgt_i\n", + " for tgt_i in opps\n", + " if not isinstance(tgt_i[\"object\"], str)\n", + " and tgt_i[\"object\"].id == target_index\n", + " ]\n", + " if len(opp) > 0:\n", + " opp = opp[0]\n", + " else:\n", + " for i in range(5):\n", + " opps = sat.find_next_opportunities(\n", + " n=self._n_ahead[sat.name] * (i + 1),\n", + " types=\"target\",\n", + " pad=True,\n", + " )\n", + " opp = [\n", + " tgt_i\n", + " for tgt_i in opps\n", + " if not isinstance(tgt_i[\"object\"], str)\n", + " and tgt_i[\"object\"].id == target_index\n", + " ]\n", + " if len(opp) > 0:\n", + " opp = opp[0]\n", + " break\n", + "\n", + " window_start = opp[\"window\"][0] - self.unwrapped.simulator.sim_time\n", + " window_start = np.max([window_start, 0.0])\n", + " window_end = opp[\"window\"][1] - self.unwrapped.simulator.sim_time\n", + "\n", + " return (window_start, window_end)\n", + "\n", + " def get_imaging_time(\n", + " self, target_index: int, window_fraction: float, sat_name: str | None\n", + " ) -> float:\n", + " window_start, window_end = self.get_image_window(target_index, sat_name)\n", + " window_fraction = np.clip(window_fraction, 0.0, 1.0)\n", + " return np.clip(\n", + " float(window_start + (window_end - window_start) * window_fraction),\n", + " 0.5,\n", + " np.inf,\n", + " )\n", + "\n", + " def _set_action_duration(\n", + " self, action, sat_name: str | None\n", + " ) -> str | None | tuple[int, float]:\n", + " if isinstance(action, str) or action is None:\n", + " return action\n", + "\n", + " action = clean_action(action)\n", + " target_index = action[0]\n", + " duration = self.get_imaging_time(target_index, action[1], sat_name)\n", + "\n", + " return (action[0], float(duration))\n", + "\n", + " def action(\n", + " self, action: tuple[int, float]\n", + " ) -> tuple[int, float] | dict | str | None:\n", + "\n", + " if isinstance(action, dict):\n", + " action_dict = {}\n", + " for sat_name, sat_action in action.items():\n", + " action_dict[sat_name] = self._set_action_duration(sat_action, sat_name)\n", + " return action_dict\n", + "\n", + " else:\n", + " return self._set_action_duration(action, None)\n", + "\n", + "\n", + "class WrapperDiscreteToHybridAction(gym.ActionWrapper, gym.Wrapper):\n", + " \"\"\"Decode a categorical choice into a target index and imaging fraction.\n", + "\n", + " Timing options vary fastest within each target: action = target * D + timing.\n", + "\n", + " Args:\n", + " env: Imaging environment with a native hybrid action space.\n", + " discrete_imaging_fraction: Predefined timing fractions for each target.\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " env,\n", + " discrete_imaging_fraction: Iterable[float],\n", + " ) -> None:\n", + " super().__init__(env)\n", + " self.discrete_imaging_fraction = list(discrete_imaging_fraction)\n", + " self.discretization = len(self.discrete_imaging_fraction)\n", + " self.env = env\n", + " if not isinstance(env.action_space, gym.spaces.Tuple):\n", + " action_space = env.action_spaces\n", + " # Assumes all satellites have the same action space\n", + " self.n_hybrid_actions = action_space[env.satellites[0].name][0].n\n", + " else:\n", + " action_space = env.action_space[0]\n", + " self.n_hybrid_actions = action_space.n\n", + " self.n_discrete_actions = self.n_hybrid_actions * self.discretization\n", + " space = gym.spaces.Discrete(self.n_discrete_actions)\n", + " if isinstance(env.action_space, gym.spaces.Tuple):\n", + " self.action_space = space\n", + " else:\n", + " self.action_space = lambda sat_name: space\n", + "\n", + " def _split_action(self, action):\n", + " if action is None or is_no_action(action):\n", + " return None\n", + "\n", + " action = int(action)\n", + " if not 0 <= action < self.n_discrete_actions:\n", + " raise ValueError(\n", + " f\"Action index {action} out of range for {self.n_discrete_actions} choices.\"\n", + " )\n", + " target_index, continuous_action = divmod(action, self.discretization)\n", + " return (target_index, self.discrete_imaging_fraction[continuous_action])\n", + "\n", + " def action(\n", + " self, action: int | dict\n", + " ) -> (\n", + " tuple[int, float]\n", + " | tuple[str, float]\n", + " | dict[str, tuple[int, float]]\n", + " | dict[str, tuple[str, float]]\n", + " | None\n", + " ):\n", + " if isinstance(action, dict):\n", + " action_dict = {}\n", + " for sat_name, sat_action in action.items():\n", + " action_dict[sat_name] = self._split_action(sat_action)\n", + " return action_dict\n", + " else:\n", + " return self._split_action(action)\n", + "\n", + "\n", + "class WrapperActionLogging(gym.Wrapper):\n", + " def __init__(self, env):\n", + "\n", + " super().__init__(env)\n", + " self.env = env\n", + "\n", + " if not isinstance(env.action_space, gym.spaces.Tuple):\n", + " action_space = env.action_spaces\n", + " self.n_discrete = action_space[env.satellites[0].name][0].n\n", + " else:\n", + " action_space = env.action_space\n", + " self.n_discrete = action_space[0].n\n", + "\n", + " self._initialize_action_logger()\n", + "\n", + " def _initialize_action_logger(self):\n", + "\n", + " self.action_duration_logger_list = {\n", + " sat.name: {\n", + " \"duration_image\": [],\n", + " }\n", + " for sat in self.env.satellites\n", + " }\n", + "\n", + " self.action_log_discrete_count = {i: 0 for i in range(self.n_discrete)}\n", + " self.action_duration_log = {i: 0.0 for i in range(self.n_discrete)}\n", + "\n", + " def return_loggers(self):\n", + "\n", + " duration_list_image_all = [\n", + " self.action_duration_logger_list[sat.name][\"duration_image\"]\n", + " for sat in self.env.satellites\n", + " ]\n", + "\n", + " return {\n", + " \"action_log_discrete_count\": self.action_log_discrete_count,\n", + " \"action_duration_log\": self.action_duration_log,\n", + " \"action_duration_logger_list\": self.action_duration_logger_list,\n", + " \"action_duration_image_list_all\": np.concatenate(duration_list_image_all),\n", + " }\n", + "\n", + " def reset(self, *, seed: int | None = None, options: dict[str, Any] | None = None):\n", + " self._initialize_action_logger()\n", + " return self.env.reset(seed=seed, options=options)\n", + "\n", + " def _store_action_info(self, action: tuple[int, float], sat_name: str):\n", + "\n", + " action = clean_action(action)\n", + " if action is None:\n", + " return\n", + "\n", + " self.action_duration_logger_list[sat_name][\"duration_image\"].append(action[1])\n", + "\n", + " if isinstance(action[0], str):\n", + " return\n", + " discrete_action = int(action[0])\n", + " self.action_log_discrete_count[discrete_action] += 1\n", + " self.action_duration_log[discrete_action] += (\n", + " action[1] - self.action_duration_log[discrete_action]\n", + " ) / self.action_log_discrete_count[discrete_action]\n", + "\n", + " def step(self, action: dict | tuple[int, float]):\n", + "\n", + " if isinstance(action, dict):\n", + " for sat_name, sat_action in action.items():\n", + " self._store_action_info(sat_action, sat_name)\n", + " else:\n", + " self._store_action_info(action, self.env.satellite.name)\n", + "\n", + " return self.env.step(action)\n", + "\n", + "\n", + "ENV_CONFIG = dict(\n", + " n_targets=(100, 3000),\n", + " orbits=3,\n", + " quality_weight=(0.0, 1.0), # Scalar for a common weight; 0.0 disables quality.\n", + " quality_types=(\"quality_off_nadir_angle\", \"quality_periodic\"),\n", + ")\n", + "\n", + "\n", + "def reward_function(priority, quality, quality_weight=0.5):\n", + "\n", + " quality_weight = np.clip(quality_weight, 0.0, 1.0)\n", + "\n", + " quality = (1 - quality_weight) + quality_weight * np.clip(\n", + " quality, a_min=0.0, a_max=1.0\n", + " )\n", + " reward = priority * quality\n", + "\n", + " return reward" + ] + }, + { + "cell_type": "markdown", + "id": "bafcc03a", + "metadata": {}, + "source": [ + "### Create consistent training and evaluation environments\n", + "\n", + "`make_env` combines the target scenario, rewarder, and simulation settings from a common configuration. In evaluation mode it returns a `ConstellationTasking` environment with the requested number of satellites, including a per-satellite dictionary interface when only one satellite is used. With `train=True`, it returns the construction arguments for a single-satellite `SatelliteTasking` environment; the RLlib wrapper below uses these arguments to instantiate the training environment.\n", + "\n", + "`setup_wrapper_stack` then adds duration logging and converts a normalized imaging fraction into a delay in seconds. Setting `discrete_action_space=True` also adds the outer wrapper that decodes an integer into a target and one of the supplied timing fractions. Keeping environment construction and action conversion in these helpers makes the settings and action convention explicit when creating a training or evaluation instance.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cc633071", + "metadata": {}, + "outputs": [], + "source": [ + "def make_env(\n", + " env_config=None,\n", + " n_sats=1,\n", + " incremental_score=False,\n", + " n_targets: int | None = None,\n", + " train: bool = False,\n", + " time_limit: float | None = None,\n", + "):\n", + " config = {**ENV_CONFIG, **dict(env_config or {})}\n", + " env_config = dict(\n", + " scenario=QualityTargets(\n", + " n_targets=config[\"n_targets\"] if n_targets is None else n_targets,\n", + " quality_weights=config[\"quality_weight\"],\n", + " quality_function_name=config[\"quality_types\"],\n", + " ),\n", + " rewarder=QualityImageReward(\n", + " reward_fn=reward_function, incremental_score=incremental_score\n", + " ),\n", + " sim_rate=SIM_RATE,\n", + " max_step_duration=500.0,\n", + " time_limit=config[\"orbits\"] * ORBIT_PERIOD\n", + " if time_limit is None\n", + " else time_limit,\n", + " failure_penalty=0.0,\n", + " log_level=\"INFO\",\n", + " )\n", + " if train:\n", + " # Training uses a single satellite configuration\n", + " env_config.update(\n", + " dict(satellite=QualitySatellite(\"EO\", sat_args=satellite_args()))\n", + " )\n", + " return env_config\n", + " else:\n", + " env = ConstellationTasking(\n", + " satellites=[\n", + " QualitySatellite(f\"EO_{i}\", sat_args=satellite_args())\n", + " for i in range(n_sats)\n", + " ],\n", + " **env_config,\n", + " )\n", + " return env\n", + "\n", + "\n", + "def setup_wrapper_stack(\n", + " env,\n", + " discrete_action_space=False,\n", + " discrete_imaging_fraction=None,\n", + "):\n", + "\n", + " env = WrapperActionLogging(env)\n", + " env = WrapperActionDuration(env)\n", + " if discrete_action_space:\n", + " env = WrapperDiscreteToHybridAction(\n", + " env,\n", + " discrete_imaging_fraction=discrete_imaging_fraction,\n", + " )\n", + "\n", + " return env" + ] + }, + { + "cell_type": "markdown", + "id": "hybrid-quality-10", + "metadata": {}, + "source": [ + "## Try a few imaging actions\n", + "\n", + "This short rollout selects target opportunity 0, the first upcoming opportunity, and requests the midpoint of its remaining access window with `(0, 0.5)`. It uses `discrete_action_space=False`, so the action contains a target index and a timing fraction directly. The action is supplied in a dictionary keyed by satellite name, and the duration wrapper converts the fraction into seconds. This exercises the physical environment without RLlib or a trained checkpoint. Increase the loop length to run a longer rollout.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "hybrid-quality-11", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2026-10-26 21:02:03,844 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[mResetting environment with seed=3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:03,846 \u001b[0m\u001b[mscene.targets \u001b[0m\u001b[mINFO \u001b[0m\u001b[mGenerating 1742 targets\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,129 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 18600.00 seconds\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,419 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mEnvironment reset\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,421 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,422 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,422 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Villa Aberastain, AR) imaging timing: requested capture at 417.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,423 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Villa Aberastain, AR) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,424 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Villa Aberastain, AR) window enabled: 374.8 to 459.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,425 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 459.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,503 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<417.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Villa Aberastain, AR)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,505 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<417.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.024999692474987952)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,506 \u001b[0m\u001b[mcomm.communication \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<417.00> \u001b[0m\u001b[mOptimizing data communication between all pairs of satellites\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,506 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<417.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,537 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<417.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.024999692474987952)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,538 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<417.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,539 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<417.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,542 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<417.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Recreo, AR) imaging timing: requested capture at 458.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,544 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<417.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Recreo, AR) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,547 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<417.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Recreo, AR) window enabled: 366.0 to 500.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,547 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<417.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 500.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,561 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<461.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Recreo, AR)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,562 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<461.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.49907471357643646)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,563 \u001b[0m\u001b[mcomm.communication \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<461.00> \u001b[0m\u001b[mOptimizing data communication between all pairs of satellites\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,564 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<461.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,594 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<461.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.49907471357643646)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,595 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<461.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,596 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<461.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,596 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<461.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Huinca Renancó, AR) imaging timing: requested capture at 500.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,597 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<461.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Huinca Renancó, AR) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,598 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<461.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Huinca Renancó, AR) window enabled: 467.5 to 533.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,598 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<461.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 533.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,608 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<510.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Huinca Renancó, AR)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,608 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<510.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.3474319715528675)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,609 \u001b[0m\u001b[mcomm.communication \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<510.00> \u001b[0m\u001b[mOptimizing data communication between all pairs of satellites\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,610 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<510.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,642 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<510.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.3474319715528675)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,643 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<510.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,644 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<510.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,645 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<510.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Romang, AR) imaging timing: requested capture at 525.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,645 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<510.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Romang, AR) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,646 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<510.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Romang, AR) window enabled: 463.8 to 540.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,647 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<510.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 540.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,657 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<541.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 540.6 for Target(Romang, AR) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,658 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<541.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,658 \u001b[0m\u001b[mcomm.communication \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<541.00> \u001b[0m\u001b[mOptimizing data communication between all pairs of satellites\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,659 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<541.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,686 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<541.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,687 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<541.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,687 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<541.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,688 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<541.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Rufino, AR) imaging timing: requested capture at 556.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,688 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<541.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Rufino, AR) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,689 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<541.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Rufino, AR) window enabled: 457.1 to 571.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,690 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<541.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 571.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,700 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<571.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 571.5 for Target(Rufino, AR) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,701 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<571.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,701 \u001b[0m\u001b[mcomm.communication \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<571.50> \u001b[0m\u001b[mOptimizing data communication between all pairs of satellites\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,702 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<571.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:04,731 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<571.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n" + ] + } + ], + "source": [ + "env = make_env()\n", + "\n", + "env = setup_wrapper_stack(\n", + " env,\n", + " discrete_action_space=False,\n", + " discrete_imaging_fraction=IMAGING_FRACTIONS,\n", + ")\n", + "\n", + "observation, info = env.reset(seed=3)\n", + "try:\n", + " for _ in range(5):\n", + " # Each satellite receives (target index, window fraction).\n", + " # With the discrete wrapper, each dictionary value is an action index.\n", + " action = {env.satellites[0].name: (0, 0.5)}\n", + " observation, reward, terminated, truncated, info = env.step(action)\n", + " if all(terminated.values()) or all(truncated.values()):\n", + " break\n", + "finally:\n", + " env.close()" + ] + }, + { + "cell_type": "markdown", + "id": "hybrid-quality-12", + "metadata": {}, + "source": [ + "## Categorical policy over target/timing pairs\n", + "\n", + "Because timing choices are discrete, the policy only needs RLlib's standard `TorchCategorical`. The actor produces one logit per target and timing option, then flattens them into the same target-major order as the wrapper. A single softmax represents the joint distribution $\\pi(i,k\\mid o)$, so timing preferences can depend on the target without a custom conditional distribution class.\n", + "\n", + "A `MultiDiscrete([N, D])` policy with separate categorical factors would impose independence between target and timing choices. The flattened categorical avoids that restriction. A custom distribution would become useful if capture times were continuous and conditioned on the chosen target.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "hybrid-quality-13", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-10-26 21:02:05,406\tINFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n", + "2026-10-26 21:02:07,388\tINFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n" + ] + } + ], + "source": [ + "from ray.rllib.algorithms.ppo import PPOConfig\n", + "from ray.rllib.algorithms.ppo.torch.ppo_torch_rl_module import PPOTorchRLModule\n", + "from ray.rllib.core import Columns\n", + "from ray.rllib.core.rl_module.rl_module import RLModuleSpec\n", + "from ray.rllib.models.torch.torch_distributions import TorchCategorical\n", + "from ray.rllib.utils.framework import try_import_torch\n", + "\n", + "from bsk_rl.utils.rllib.discounting import TimeDiscountedGAEPPOTorchLearner\n", + "\n", + "torch, nn = try_import_torch()" + ] + }, + { + "cell_type": "markdown", + "id": "hybrid-quality-15", + "metadata": {}, + "source": [ + "## Configurable actor, critic, and combined RL module\n", + "\n", + "The following definitions construct a custom actor, which outputs $ND$ logits, and a custom critic:\n", + "\n", + "* **Actor:** separate target and satellite MLPs; multi-head target self-attention with a residual connection and LayerNorm; mean/max target pooling; and one logit per target/timing pair.\n", + "* **Critic:** a separate target MLP, mean/max pooling, and an MLP combining these embeddings with the raw satellite/context features to predict a scalar value.\n", + "\n", + "Both networks accept keyword arguments for their dimensions, depths, activation, and dropout. `HybridQualityModule` combines them and reads those arguments from\n", + "RLlib's `model_config_dict`; critic arguments use the `critic_` prefix. The module checks that its target count and timing discretization agree with the environment. The training forward pass returns logits and values so PPO can evaluate the actions stored in its rollout batch." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "hybrid-quality-16", + "metadata": {}, + "outputs": [], + "source": [ + "def build_mlp(\n", + " input_size,\n", + " width,\n", + " depth,\n", + " output_size,\n", + " *,\n", + " act_function=\"ReLU\",\n", + " dropout=0.0,\n", + " dropout_before_output=0.0,\n", + " output_activation=False,\n", + "):\n", + " \"\"\"Configurable hidden layers, preserving the research parameter layout.\"\"\"\n", + " if depth < 0 or width <= 0 or output_size <= 0:\n", + " raise ValueError(\"MLP dimensions must be positive and depth nonnegative.\")\n", + " activation = getattr(nn, act_function)\n", + " layers = []\n", + " for _ in range(depth):\n", + " layers.extend([nn.Linear(input_size, width), activation()])\n", + " if dropout:\n", + " layers.append(nn.Dropout(dropout))\n", + " input_size = width\n", + " if dropout_before_output:\n", + " layers.append(nn.Dropout(dropout_before_output))\n", + " layers.append(nn.Linear(input_size, output_size))\n", + " if output_activation:\n", + " layers.append(activation())\n", + " return nn.Sequential(*layers)\n", + "\n", + "\n", + "class TargetSelfAttention(nn.Module):\n", + " def __init__(self, input_dim, attention_dim, num_heads):\n", + " super().__init__()\n", + " if num_heads <= 0 or attention_dim <= 0 or attention_dim % num_heads:\n", + " raise ValueError(\n", + " \"attention_dim must be positive and divisible by num_heads.\"\n", + " )\n", + " self.num_heads = num_heads\n", + " self.head_dim = attention_dim // num_heads\n", + " self.attention_dim = attention_dim\n", + " self.qkv = nn.Linear(input_dim, 3 * attention_dim, bias=False)\n", + " self.out = nn.Linear(attention_dim, input_dim, bias=False)\n", + "\n", + " def forward(self, tokens):\n", + " batch_size, n_targets, _ = tokens.shape\n", + " qkv = self.qkv(tokens).view(\n", + " batch_size, n_targets, 3, self.num_heads, self.head_dim\n", + " )\n", + " q, k, v = qkv.permute(2, 0, 3, 1, 4).unbind(dim=0)\n", + " attended = nn.functional.scaled_dot_product_attention(\n", + " q, k, v, dropout_p=0.0, is_causal=False\n", + " )\n", + " attended = (\n", + " attended.transpose(1, 2)\n", + " .contiguous()\n", + " .view(batch_size, n_targets, self.attention_dim)\n", + " )\n", + " return self.out(attended)\n", + "\n", + "\n", + "class ImagingActor(nn.Module):\n", + " def __init__(\n", + " self,\n", + " target_size,\n", + " context_size,\n", + " n_times,\n", + " *,\n", + " width_phi=256,\n", + " depth_phi=2,\n", + " tgt_encoded_dim=64,\n", + " attention_dim=64,\n", + " num_heads=2,\n", + " width_phi_sat=256,\n", + " depth_phi_sat=2,\n", + " sat_encoded_dim=64,\n", + " width_psi_img=128,\n", + " depth_psi_img=4,\n", + " act_function=\"ReLU\",\n", + " dropout=0.0,\n", + " psi_img_dropout_last_layer=0.1,\n", + " ):\n", + " super().__init__()\n", + " self.phi = build_mlp(\n", + " target_size,\n", + " width_phi,\n", + " depth_phi,\n", + " tgt_encoded_dim,\n", + " act_function=act_function,\n", + " dropout=dropout,\n", + " )\n", + " self.self_attention = TargetSelfAttention(\n", + " tgt_encoded_dim, attention_dim, num_heads\n", + " )\n", + " self.norm_self_attention = nn.LayerNorm(tgt_encoded_dim)\n", + " self.psi_img = build_mlp(\n", + " 3 * tgt_encoded_dim + sat_encoded_dim,\n", + " width_psi_img,\n", + " depth_psi_img,\n", + " tgt_encoded_dim,\n", + " act_function=act_function,\n", + " dropout=dropout,\n", + " dropout_before_output=psi_img_dropout_last_layer,\n", + " output_activation=True,\n", + " )\n", + " self.out_layer_psi_img = nn.Linear(tgt_encoded_dim, n_times)\n", + " self.phi_sat = build_mlp(\n", + " context_size,\n", + " width_phi_sat,\n", + " depth_phi_sat,\n", + " sat_encoded_dim,\n", + " act_function=act_function,\n", + " dropout=dropout,\n", + " )\n", + "\n", + " def forward(self, context, targets):\n", + " tokens = self.phi(targets)\n", + " satellite = self.phi_sat(context)\n", + " tokens = self.norm_self_attention(tokens + self.self_attention(tokens))\n", + " n_targets = tokens.shape[1]\n", + " mean_targets = tokens.mean(dim=1, keepdim=True).expand(-1, n_targets, -1)\n", + " max_targets = tokens.max(dim=1, keepdim=True).values.expand(-1, n_targets, -1)\n", + " features = torch.cat(\n", + " [\n", + " tokens,\n", + " mean_targets,\n", + " max_targets,\n", + " satellite.unsqueeze(1).expand(-1, n_targets, -1),\n", + " ],\n", + " dim=-1,\n", + " )\n", + " logits = self.out_layer_psi_img(self.psi_img(features))\n", + " return logits.flatten(start_dim=1)\n", + "\n", + "\n", + "class ImagingCritic(nn.Module):\n", + " def __init__(\n", + " self,\n", + " target_size,\n", + " context_size,\n", + " *,\n", + " width_phi=256,\n", + " depth_phi=2,\n", + " tgt_encoded_dim=128,\n", + " width_psi=64,\n", + " depth_psi=3,\n", + " act_function=\"ReLU\",\n", + " dropout=0.0,\n", + " ):\n", + " super().__init__()\n", + " self.phi = build_mlp(\n", + " target_size,\n", + " width_phi,\n", + " depth_phi,\n", + " tgt_encoded_dim,\n", + " act_function=act_function,\n", + " dropout=dropout,\n", + " )\n", + " self.psi = build_mlp(\n", + " context_size + 2 * tgt_encoded_dim,\n", + " width_psi,\n", + " depth_psi,\n", + " 1,\n", + " act_function=act_function,\n", + " dropout=dropout,\n", + " )\n", + "\n", + " def forward(self, context, targets):\n", + " tokens = self.phi(targets)\n", + " features = torch.cat(\n", + " [context, tokens.mean(dim=1), tokens.max(dim=1).values], dim=-1\n", + " )\n", + " return self.psi(features).squeeze(-1)\n", + "\n", + "\n", + "class HybridQualityModule(PPOTorchRLModule):\n", + " \"\"\"Combine independently configurable actor and critic heads for PPO.\"\"\"\n", + "\n", + " def setup(self):\n", + " config = self.config.model_config_dict\n", + " self.n_targets = int(config[\"n_targets\"])\n", + " self.context_size = int(config[\"obs_sat\"])\n", + " self.n_times = int(config[\"discretization\"])\n", + " target_features = self.config.observation_space.shape[0] - self.context_size\n", + " if (\n", + " self.n_targets <= 0\n", + " or self.context_size < 0\n", + " or target_features <= 0\n", + " or target_features % self.n_targets\n", + " ):\n", + " raise ValueError(\n", + " \"Observation size must match context plus equal target tokens.\"\n", + " )\n", + " self.target_size = target_features // self.n_targets\n", + " if (\n", + " not isinstance(self.config.action_space, gym.spaces.Discrete)\n", + " or self.n_times <= 0\n", + " or self.config.action_space.n != self.n_targets * self.n_times\n", + " ):\n", + " raise ValueError(\"Expected Discrete(n_targets * discretization) actions.\")\n", + " actor_keys = (\n", + " \"width_phi\",\n", + " \"depth_phi\",\n", + " \"tgt_encoded_dim\",\n", + " \"attention_dim\",\n", + " \"num_heads\",\n", + " \"width_phi_sat\",\n", + " \"depth_phi_sat\",\n", + " \"sat_encoded_dim\",\n", + " \"width_psi_img\",\n", + " \"depth_psi_img\",\n", + " \"act_function\",\n", + " \"dropout\",\n", + " \"psi_img_dropout_last_layer\",\n", + " )\n", + " self.pi_head = ImagingActor(\n", + " self.target_size,\n", + " self.context_size,\n", + " self.n_times,\n", + " **{key: config[key] for key in actor_keys if key in config},\n", + " )\n", + " self.vf = ImagingCritic(\n", + " self.target_size,\n", + " self.context_size,\n", + " width_phi=config.get(\"critic_width_phi\", 256),\n", + " depth_phi=config.get(\"critic_depth_phi\", 2),\n", + " tgt_encoded_dim=config.get(\"critic_tgt_encoded_dim\", 128),\n", + " width_psi=config.get(\"critic_width_psi\", 64),\n", + " depth_psi=config.get(\"critic_depth_psi\", 3),\n", + " act_function=config.get(\"act_function\", \"ReLU\"),\n", + " dropout=config.get(\"critic_dropout\", config.get(\"dropout\", 0.0)),\n", + " )\n", + " self.action_dist_cls = TorchCategorical\n", + " self._inference_only_state_dict_keys = {}\n", + "\n", + " def get_initial_state(self):\n", + " return {}\n", + "\n", + " def split_observation(self, batch):\n", + " values = batch[Columns.OBS].float()\n", + " context = values[:, : self.context_size]\n", + " targets = values[:, self.context_size :].reshape(\n", + " -1, self.n_targets, self.target_size\n", + " )\n", + " return context, targets\n", + "\n", + " def compute_values(self, batch, embeddings=None):\n", + " return self.vf(*self.split_observation(batch))\n", + "\n", + " def _forward_inference(self, batch, **kwargs):\n", + " return {\n", + " Columns.ACTION_DIST_INPUTS: self.pi_head(*self.split_observation(batch))\n", + " }\n", + "\n", + " def _forward_exploration(self, batch, **kwargs):\n", + " return self._forward_train(batch)\n", + "\n", + " def _forward_train(self, batch, **kwargs):\n", + " context, targets = self.split_observation(batch)\n", + " return {\n", + " Columns.ACTION_DIST_INPUTS: self.pi_head(context, targets),\n", + " Columns.VF_PREDS: self.vf(context, targets),\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "hybrid-quality-17", + "metadata": {}, + "source": [ + "## PPO configuration\n", + "\n", + "The configuration below supplies the model dimensions and PPO hyperparameters for three timing options per target. `Env_wrapped` adapts the custom environment for RLlib: it accepts `env_config`, constructs the single-satellite environment, applies the action-wrapper stack, and initializes the optional episode-data logging callbacks. Passing the class directly as `env=Env_wrapped` lets RLlib create each environment instance without registering a separate Gym environment ID.\n", + "\n", + "`TimeDiscountedGAEPPOTorchLearner` uses the reported step duration `d_ts`, in seconds, to apply the duration-dependent discount $\\gamma^{\\Delta t}$. Here `gamma=0.995` is the per-second factor, and the learner discounts raw capture rewards at the end of each step. The evaluation loop accumulates undiscounted rewards, as in the paper's reported comparisons.\n", + "\n", + "Defining `ppo_config` does not build or train an algorithm. The inference demonstration at the end of the cell creates a separate wrapped environment and a fresh module with random initial weights, then executes one categorical action. Its output checks the inference interface and does not represent trained-policy performance.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "hybrid-quality-18", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2026-10-26 21:02:08,848 \u001b[0m\u001b[m \u001b[0m\u001b[93mWARNING \u001b[0m\u001b[93mCreating logger for new env on PID=71679. Old environments in process may now log times incorrectly.\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:08,851 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[mResetting environment with seed=7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:08,855 \u001b[0m\u001b[mscene.targets \u001b[0m\u001b[mINFO \u001b[0m\u001b[mGenerating 390 targets\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:08,986 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 18600.00 seconds\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,092 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mEnvironment reset\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,141 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,142 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 38 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,143 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bougara, DZ) imaging timing: requested capture at 3638.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,144 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bougara, DZ) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,144 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bougara, DZ) window enabled: 3589.0 to 3688.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,145 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 3688.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,234 \u001b[0m\u001b[msim.simulator \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<500.00> \u001b[0m\u001b[mMax step duration reached\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,235 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<500.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,235 \u001b[0m\u001b[mcomm.communication \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<500.00> \u001b[0m\u001b[mOptimizing data communication between all pairs of satellites\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,267 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<500.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Untrained policy: 115 Target: 38 Imaging fraction: 0.5 Reward: {'EO_0': 0.0}\n" + ] + } + ], + "source": [ + "from bsk_rl.utils.rllib.callbacks import EpisodeDataLogger\n", + "\n", + "MODEL_CONFIG = {\n", + " \"n_targets\": N_AHEAD,\n", + " \"obs_sat\": 32,\n", + " \"discretization\": len(IMAGING_FRACTIONS),\n", + " \"width_phi\": 256,\n", + " \"depth_phi\": 2,\n", + " \"tgt_encoded_dim\": 64,\n", + " \"attention_dim\": 64,\n", + " \"num_heads\": 2,\n", + " \"width_phi_sat\": 256,\n", + " \"depth_phi_sat\": 2,\n", + " \"sat_encoded_dim\": 64,\n", + " \"width_psi_img\": 128,\n", + " \"depth_psi_img\": 4,\n", + " \"act_function\": \"ReLU\",\n", + " \"dropout\": 0.0,\n", + " \"psi_img_dropout_last_layer\": 0.1,\n", + " \"critic_tgt_encoded_dim\": 128,\n", + " \"critic_width_phi\": 256,\n", + " \"critic_depth_phi\": 2,\n", + " \"critic_width_psi\": 64,\n", + " \"critic_depth_psi\": 3,\n", + "}\n", + "TRAINING_CONFIG = {\n", + " \"gamma\": 0.995,\n", + " \"lr\": [[0, 0.00012122027544761331], [117126, 3.707168645103191e-06]],\n", + " \"entropy_coeff\": [[0, 0.047466805372264546], [734487, 0.0]],\n", + " \"train_batch_size\": 9000,\n", + " \"num_sgd_iter\": 15,\n", + " \"lambda_\": 0.8713548569911232,\n", + " \"use_kl_loss\": False,\n", + " \"clip_param\": 0.24943882616708635,\n", + " \"grad_clip\": 0.3104924935285628,\n", + " \"vf_clip_param\": 2.0,\n", + "}\n", + "\n", + "\n", + "module_spec = RLModuleSpec(\n", + " module_class=HybridQualityModule, model_config_dict=dict(MODEL_CONFIG)\n", + ")\n", + "\n", + "\n", + "class Env_wrapped(EpisodeDataLogger, gym.Wrapper):\n", + " def __init__(self, env_config):\n", + "\n", + " env_config = dict(env_config)\n", + " episode_data_callback = env_config.pop(\"episode_data_callback\", None)\n", + " satellite_data_callback = env_config.pop(\"satellite_data_callback\", None)\n", + " env_args = make_env(env_config, n_sats=1, train=True)\n", + " env = SatelliteTasking(**env_args)\n", + " env = WrapperActionLogging(\n", + " env,\n", + " )\n", + " env = WrapperActionDuration(\n", + " env,\n", + " )\n", + " env = WrapperDiscreteToHybridAction(\n", + " env,\n", + " discrete_imaging_fraction=IMAGING_FRACTIONS,\n", + " )\n", + " EpisodeDataLogger.__init__(self, episode_data_callback, satellite_data_callback)\n", + " gym.Wrapper.__init__(self, env)\n", + "\n", + "\n", + "ppo_config = (\n", + " PPOConfig()\n", + " .environment(\n", + " env=Env_wrapped,\n", + " env_config=dict(ENV_CONFIG),\n", + " normalize_actions=False,\n", + " clip_actions=False,\n", + " )\n", + " .framework(\"torch\")\n", + " .api_stack(\n", + " enable_rl_module_and_learner=True,\n", + " enable_env_runner_and_connector_v2=True,\n", + " )\n", + " .env_runners(num_env_runners=0, rollout_fragment_length=32)\n", + " .training(learner_class=TimeDiscountedGAEPPOTorchLearner, **TRAINING_CONFIG)\n", + " .rl_module(rl_module_spec=module_spec)\n", + ")\n", + "\n", + "env = make_env()\n", + "\n", + "env = setup_wrapper_stack(\n", + " env,\n", + " discrete_action_space=True,\n", + " discrete_imaging_fraction=IMAGING_FRACTIONS,\n", + ")\n", + "\n", + "env.reset(seed=7)\n", + "module = RLModuleSpec(\n", + " module_class=HybridQualityModule,\n", + " observation_space=env.observation_space(env.satellites[0].name),\n", + " action_space=env.action_space(env.satellites[0].name),\n", + " model_config_dict=dict(MODEL_CONFIG),\n", + ").build()\n", + "\n", + "\n", + "def policy(satellite, *args, **kwargs):\n", + " obs = torch.as_tensor(satellite.get_obs())\n", + "\n", + " module.eval()\n", + " with torch.no_grad():\n", + " output = module.forward_inference({Columns.OBS: obs[None]})\n", + " distribution = module.get_inference_action_dist_cls().from_logits(\n", + " output[Columns.ACTION_DIST_INPUTS]\n", + " )\n", + " action = distribution.to_deterministic().sample()\n", + "\n", + " return int(action.item())\n", + "\n", + "\n", + "action = policy(env.satellites[0])\n", + "\n", + "_, reward, _, _, info = env.step({env.satellites[0].name: action})\n", + "target_index, timing_index = divmod(action, len(IMAGING_FRACTIONS))\n", + "print(\n", + " \"Untrained policy:\",\n", + " action,\n", + " \"Target:\",\n", + " target_index,\n", + " \"Imaging fraction:\",\n", + " IMAGING_FRACTIONS[timing_index],\n", + " \"Reward:\",\n", + " reward,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cc0d54d2", + "metadata": {}, + "source": [ + "## Multi-satellite coordination through communication\n", + "\n", + "A policy trained for one satellite can be evaluated independently on each satellite, with coordination handled by the decision loop below. This example models ideal, instantaneous sharing of completed acquisitions and intended tasks. It does not simulate message transmission delays or communication outages. The `policy` function below uses deterministic inference from the **untrained module** created above; load trained weights to evaluate a learned RL policy.\n", + "\n", + "An **intent** identifies a target another satellite is currently tasked to image. Reward-aware intents also carry the **expected absolute reward** at the requested acquisition time. This prediction is not an obtained reward and does not guarantee capture at the requested time.\n", + "\n", + "The human-readable mode names correspond to these `multisat_comm_config[\"mode\"]` values:\n", + "\n", + "| Mode | Code value | Coordination rule |\n", + "|---|---|---|\n", + "| **Intent** | `\"intent\"` | Exclude globally imaged targets and targets currently intended by other satellites. A new decision selects from the remaining targets; there is no comparison of competing reward predictions. |\n", + "| **Intent-incremental** | `\"incremental_intent\"` | Keep re-imaging candidates when their predicted reward can exceed both the best completed reward and the largest relevant competing intent reward. Store this maximum in bookkeeping and use the clipped marginal profile to test whether an opportunity has a useful improvement. Several satellites may retain intents for the same target; this mode does not forcibly choose a single claimant. |\n", + "| **Intent-greedy** | `\"greedy\"` | Exclude globally imaged targets. If a satellite selects an already intended target, compare the two expected **absolute** rewards at their respective requested acquisition times. A strictly larger prediction replaces the previous claimant and schedules that satellite to choose again. Otherwise, temporarily exclude the contested target for the challenger and ask it to choose again. Equal predictions retain the existing claimant. |\n", + "| **Intent-greedy-incremental** | `\"greedy_incremental\"` | Use the same conflict-resolution rule as intent-greedy while allowing useful re-imaging. The marginal profile is measured against the best **completed** reward; pending intents are handled by the explicit greedy comparison rather than subtracted from this baseline. |\n", + "\n", + "The additional `\"imaged\"` mode shares completed acquisitions only: it excludes globally imaged targets without reserving targets through intents. Nonincremental modes are intended to use `incremental_score=False`; the two incremental modes require `incremental_score=True`. Select rewarder scoring explicitly when creating the environment—the communication-mode string does not change it automatically.\n", + "\n", + "For a given target and the satellite currently choosing an action, intent-incremental computes:\n", + "\n", + "```python\n", + "baseline = max(best_obtained_reward, highest_competing_intent_reward)\n", + "marginal_reward = max(absolute_reward_at_t - baseline, 0)\n", + "```\n", + "\n", + "With no competing intent, the baseline is simply the best obtained reward. Intent-greedy-incremental uses that completed-reward baseline and resolves competing intents separately. Advertising an intent does not increase obtained reward. Actual incremental reward is the improvement over the best completed reward, not over an intent prediction.\n", + "\n", + "Before each decision, the loop refreshes the satellite's completed/intended bookkeeping and clears the cached observation. Intent-based exclusion changes which target opportunities are available. Incremental modes additionally use `estimate_quality_profile(..., return_rewards=True)` to retain opportunities whose maximum predicted marginal reward exceeds `0.001`. This is an opportunity-level check; it does not guarantee a positive reward at the policy's chosen timing option. `cooldown_s` is a separate intended restriction on rapid captures of the same target by the same satellite.\n", + "\n", + "**Observation wiring:** `QualitySatellite` binds its spline observation to `estimate_quality_profile`. After the loop updates bookkeeping and clears the observation cache, the next observation fits the marginal profile using that satellite's current completed/intended reward baseline. The estimator caches absolute predictions but subtracts the current bookkeeping baseline on every call, so same-time bookkeeping changes affect the spline coefficients. This changes observation values while preserving the 512-value observation shape. The successful-capture handler also records `last_img_time_per_sat`, which supplies the cooldown filter.\n" + ] + }, + { + "cell_type": "markdown", + "id": "cb564573", + "metadata": {}, + "source": [ + "### Helpers for decoding and comparing imaging actions\n", + "\n", + "`get_imaging_action` decodes an action into a target index and normalized timing fraction, or returns `None` when no imaging action is supplied. `get_time` uses the duration wrapper to convert that fraction into a delay in seconds for the selected satellite and target opportunity.\n", + "\n", + "`get_expected_absolute_reward` adds the current simulation time to this delay and evaluates the predicted absolute reward at the requested acquisition time. The coordination loop uses this prediction to advertise reward-aware intents and compare competing tasks. It is evaluated independently of the bookkeeping baseline: marginal reward is computed separately from completed or intended rewards. The requested time remains a prediction point; it does not guarantee that the instrument will capture the image at that time.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "25a4f434", + "metadata": {}, + "outputs": [], + "source": [ + "def get_expected_absolute_reward(env, satellite, target_index, action_fraction):\n", + " \"\"\"Predict absolute reward at the wrapper-decoded desired acquisition time.\n", + "\n", + " The wrapper returns a delay, whereas quality functions expect absolute\n", + " simulator time. The prediction is independent of reward bookkeeping and\n", + " does not guarantee acquisition at the desired time or within the window.\n", + " \"\"\"\n", + " target = satellite.parse_target_selection(target_index)\n", + " imaging_delay = float(\n", + " env.get_imaging_time(target_index, action_fraction, satellite.name)\n", + " )\n", + " if not np.isfinite(imaging_delay) or imaging_delay < 0.0:\n", + " raise ValueError(f\"Invalid desired imaging delay: {imaging_delay}\")\n", + " desired_time = satellite.simulator.sim_time + imaging_delay\n", + " predicted_reward = estimate_absolute_reward(\n", + " satellite, target, np.array([desired_time], dtype=float)\n", + " )\n", + " if predicted_reward.size != 1 or not np.all(np.isfinite(predicted_reward)):\n", + " raise ValueError(\n", + " f\"Invalid expected acquisition reward for target {target.id}: \"\n", + " f\"{predicted_reward}\"\n", + " )\n", + " return float(predicted_reward.item())\n", + "\n", + "\n", + "def get_imaging_action(env, action):\n", + " \"\"\"Decode a target index, or return None for a non-imaging action.\"\"\"\n", + " action_parsed = (\n", + " env._split_action(action)\n", + " if isinstance(env, WrapperDiscreteToHybridAction)\n", + " else action\n", + " )\n", + " if action_parsed is None:\n", + " return None\n", + " tgt_index, fraction = action_parsed\n", + " return tgt_index, fraction\n", + "\n", + "\n", + "def get_time(env, action: int | tuple[int, float], sat_name: str) -> float:\n", + "\n", + " action_parsed = get_imaging_action(env, action)\n", + " if action_parsed is None:\n", + " raise ValueError(\"Non-imaging actions do not have an imaging time.\")\n", + "\n", + " action_time = env.get_imaging_time(action_parsed[0], action_parsed[1], sat_name)\n", + "\n", + " return action_time" + ] + }, + { + "cell_type": "markdown", + "id": "c73f1e2f", + "metadata": {}, + "source": [ + "### Configure satellite access filters\n", + "\n", + "The following block creates the constellation and configures each satellite's access filters for the selected communication mode. These filters determine which target opportunities are available when forming observations and selecting actions. Depending on the mode, they exclude completed or competing intended targets, temporarily exclude targets lost in greedy comparisons, or retain opportunities with useful predicted marginal reward.\n", + "\n", + "The displayed configuration uses two satellites, 1000 targets, and `incremental_intent` with incremental scoring enabled. Its filters require a maximum predicted marginal reward greater than `0.001` over the remaining window and more than `cooldown_s=15.0` seconds since the same satellite last captured that target. These checks select eligible opportunities; they do not guarantee positive reward at the chosen timing fraction. The 15-second cooldown is an explicit setting of this example; the current paper draft does not specify a cooldown value.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1eed896", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2026-10-26 21:02:09,304 \u001b[0m\u001b[m \u001b[0m\u001b[93mWARNING \u001b[0m\u001b[93mCreating logger for new env on PID=71679. Old environments in process may now log times incorrectly.\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,307 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[mResetting environment with seed=1042\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,311 \u001b[0m\u001b[mscene.targets \u001b[0m\u001b[mINFO \u001b[0m\u001b[mGenerating 1000 targets\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,499 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 1800.00 seconds\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,561 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mFinding opportunity windows from 0.00 to 1800.00 seconds\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,647 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mFinding opportunity windows from 1800.00 to 4200.00 seconds\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,703 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mFinding opportunity windows from 1800.00 to 4200.00 seconds\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,837 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[mEnvironment reset\u001b[0m\n" + ] + } + ], + "source": [ + "from copy import copy\n", + "\n", + "setup_env_args = {}\n", + "incremental_score = True\n", + "cooldown_s = 15.0 # set to 0 to disable cooldown\n", + "seed = 42\n", + "n_targets = 1000\n", + "n_sats = 2\n", + "multisat_comm_config = {\"mode\": \"incremental_intent\"}\n", + "\n", + "env = make_env(\n", + " setup_env_args,\n", + " n_sats=n_sats,\n", + " incremental_score=incremental_score,\n", + " n_targets=n_targets,\n", + " time_limit=ORBIT_PERIOD * 0.1,\n", + ")\n", + "\n", + "env = setup_wrapper_stack(\n", + " env, discrete_action_space=True, discrete_imaging_fraction=IMAGING_FRACTIONS\n", + ")\n", + "\n", + "seed_case = seed + n_targets\n", + "_, info = env.reset(seed=seed_case)\n", + "\n", + "sat_last_decision_time = {sat.name: 0.0 for sat in env.satellites}\n", + "reward_cumulative = 0.0\n", + "\n", + "intents = {sat.name: None for sat in env.satellites}\n", + "intents_quality = {sat.name: None for sat in env.satellites}\n", + "\n", + "comm_mode = multisat_comm_config[\"mode\"]\n", + "if len(env.satellites) > 1:\n", + "\n", + " def filter_intent(opp, sat):\n", + " return opp[\"object\"] not in sat.intent_set_shared\n", + "\n", + " def filter_imaged(opp, sat):\n", + " return opp[\"object\"] not in sat.imaged_set_shared\n", + "\n", + " def filter_minimum_reward(opp, sat):\n", + " rewards = estimate_quality_profile(sat, opp, return_rewards=True)\n", + " minimum_quality = np.max(rewards) > 1e-3\n", + " return minimum_quality\n", + "\n", + " def filter_time(opp, sat):\n", + " tgt = opp[\"object\"]\n", + " current_time = sat.simulator.sim_time\n", + " time_diff = current_time - tgt.last_img_time_per_sat.get(sat.name, -np.inf)\n", + " return time_diff > cooldown_s\n", + "\n", + " def filter_lower_reward(opp, sat):\n", + " return opp[\"object\"] not in sat.temp_filter_set\n", + "\n", + " for sat in env.satellites:\n", + " sat.intent_set_shared = set()\n", + " sat.imaged_set_shared = set()\n", + "\n", + " if comm_mode == \"imaged\":\n", + " for sat in env.satellites:\n", + " sat.add_access_filter(lambda opp, sat=sat: filter_imaged(opp, sat))\n", + "\n", + " elif comm_mode == \"intent\":\n", + " for sat in env.satellites:\n", + " sat.add_access_filter(lambda opp, sat=sat: filter_intent(opp, sat))\n", + " sat.add_access_filter(lambda opp, sat=sat: filter_imaged(opp, sat))\n", + "\n", + " elif comm_mode == \"incremental_intent\":\n", + " for sat in env.satellites:\n", + " if multisat_comm_config.get(\"filter_minimum_quality\", True):\n", + " sat.add_access_filter(\n", + " lambda opp, sat=sat: filter_minimum_reward(opp, sat)\n", + " )\n", + " if cooldown_s > 0:\n", + " sat.add_access_filter(lambda opp, sat=sat: filter_time(opp, sat))\n", + "\n", + " elif comm_mode == \"greedy\":\n", + " # Retask an existing claimant when another satellite predicts higher reward.\n", + "\n", + " for sat in env.satellites:\n", + " sat.temp_filter_set = set()\n", + " sat.add_access_filter(lambda opp, sat=sat: filter_imaged(opp, sat))\n", + " sat.add_access_filter(lambda opp, sat=sat: filter_lower_reward(opp, sat))\n", + "\n", + " elif comm_mode == \"greedy_incremental\":\n", + " for sat in env.satellites:\n", + " sat.temp_filter_set = set()\n", + " sat.add_access_filter(lambda opp, sat=sat: filter_lower_reward(opp, sat))\n", + " if multisat_comm_config.get(\"filter_minimum_quality\", True):\n", + " sat.add_access_filter(\n", + " lambda opp, sat=sat: filter_minimum_reward(opp, sat)\n", + " )\n", + " if cooldown_s > 0:\n", + " sat.add_access_filter(lambda opp, sat=sat: filter_time(opp, sat))" + ] + }, + { + "cell_type": "markdown", + "id": "bfbee619", + "metadata": {}, + "source": [ + "### Run the coordination loop\n", + "\n", + "At each step, the loop identifies satellites that require a new task, refreshes their shared acquisition information and mode-specific bookkeeping, and clears cached observations before selecting actions. It records new intents and their expected rewards; greedy modes can also displace a competing intent and request another decision. The action dictionary advances the constellation, rewards are accumulated across satellites, and the loop stops when the environment reports termination or truncation.\n", + "\n", + "The current `policy` runs the actor defined above with randomly initialized weights and selects its highest-logit target/timing pair through a deterministic categorical distribution. It therefore demonstrates coordination with an untrained policy, rather than uniformly sampling random actions. Load trained module weights to evaluate a learned policy with the same loop.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "37c501d9", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[90;3m2026-10-26 21:02:09,946 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,948 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 3 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,948 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Māhta, IN) imaging timing: requested capture at 11.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,949 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Māhta, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,950 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Māhta, IN) window enabled: 0.0 to 23.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,950 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 23.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,952 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,953 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(San Sebastián de la Gomera, ES) imaging timing: requested capture at 430.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,953 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(San Sebastián de la Gomera, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,954 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(San Sebastián de la Gomera, ES) window enabled: 371.5 to 489.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,955 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<0.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 489.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,967 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<24.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 23.7 for Target(Māhta, IN) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,968 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<24.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:09,969 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<24.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,037 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<24.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,066 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<24.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,067 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<24.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,069 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<24.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Nawāshahr, IN) imaging timing: requested capture at 31.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,070 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<24.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Nawāshahr, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,071 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<24.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Nawāshahr, IN) window enabled: 0.0 to 38.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,072 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<24.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 38.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,080 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<39.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 38.9 for Target(Nawāshahr, IN) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,082 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<39.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,083 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<39.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,159 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<39.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,190 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<39.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,191 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<39.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,192 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<39.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Gadarpur, IN) imaging timing: requested capture at 66.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,192 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<39.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Gadarpur, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,193 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<39.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Gadarpur, IN) window enabled: 16.6 to 93.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,193 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<39.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 93.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,209 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<83.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Gadarpur, IN)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,211 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<83.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.5571039239604911)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,211 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<83.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,288 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<83.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.5571039239604911)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,323 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<83.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,324 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<83.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 3 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,325 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<83.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Koini, IN) imaging timing: requested capture at 149.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,326 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<83.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Koini, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,327 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<83.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Koini, IN) window enabled: 98.0 to 200.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,327 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<83.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 200.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,348 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<149.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Koini, IN)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,349 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<149.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.724379742733913)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,349 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<149.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,421 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<149.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.724379742733913)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,466 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<149.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,466 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<149.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 6 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,467 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<149.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Jitpur, NP) imaging timing: requested capture at 180.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,468 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<149.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Jitpur, NP) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,469 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<149.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Jitpur, NP) window enabled: 80.5 to 211.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,469 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<149.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 211.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,482 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Jitpur, NP)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,483 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.005747950399001029)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,484 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,551 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.005747950399001029)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,582 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,583 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 8 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,584 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bāsopatti, IN) imaging timing: requested capture at 204.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,585 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bāsopatti, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,586 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bāsopatti, IN) window enabled: 104.0 to 228.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,587 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<180.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 228.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,598 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<204.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Bāsopatti, IN)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,600 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<204.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.6400027666600737)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,601 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<204.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,680 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<204.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.6400027666600737)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,715 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<204.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,716 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<204.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 8 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,717 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<204.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Simaria, IN) imaging timing: requested capture at 217.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,717 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<204.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Simaria, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,718 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<204.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Simaria, IN) window enabled: 126.9 to 231.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,718 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<204.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 231.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,728 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<227.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Simaria, IN)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,730 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<227.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.6273504569748937)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,731 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<227.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,810 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<227.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.6273504569748937)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,878 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<227.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,879 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<227.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 14 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,882 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<227.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Kasba, IN) imaging timing: requested capture at 242.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,882 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<227.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Kasba, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,884 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<227.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Kasba, IN) window enabled: 128.7 to 257.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,884 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<227.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 257.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,893 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<249.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Kasba, IN)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,894 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<249.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.03568579182674939)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,895 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<249.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,967 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<249.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.03568579182674939)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:10,999 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<249.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,000 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<249.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,000 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<249.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Rāghopur, IN) imaging timing: requested capture at 250.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,001 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<249.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Rāghopur, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,002 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<249.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Rāghopur, IN) window enabled: 134.3 to 250.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,003 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<249.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 250.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,007 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<251.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 250.9 for Target(Rāghopur, IN) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,008 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<251.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,009 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<251.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,077 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<251.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,112 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<251.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,113 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<251.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 3 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,113 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<251.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Mohanpur Gaughāta, IN) imaging timing: requested capture at 254.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,114 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<251.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Mohanpur Gaughāta, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,114 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<251.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Mohanpur Gaughāta, IN) window enabled: 136.2 to 256.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,115 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<251.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 256.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,119 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<257.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 256.9 for Target(Mohanpur Gaughāta, IN) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,120 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<257.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,120 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<257.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,207 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<257.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,238 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<257.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,239 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<257.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 2 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,240 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<257.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Mālior, IN) imaging timing: requested capture at 261.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,240 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<257.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Mālior, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,241 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<257.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Mālior, IN) window enabled: 139.0 to 265.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,242 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<257.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 265.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,246 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<265.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 265.4 for Target(Mālior, IN) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,247 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<265.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,248 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<265.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,327 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<265.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,353 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<265.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,354 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<265.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 2 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,355 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<265.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Pemagatshel, BT) imaging timing: requested capture at 276.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,355 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<265.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Pemagatshel, BT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,356 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<265.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Pemagatshel, BT) window enabled: 166.6 to 287.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,357 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<265.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 287.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,359 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Pemagatshel, BT)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,360 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.011399764508251285)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,361 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,438 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.011399764508251285)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,464 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,465 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,466 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bāwāli, IN) imaging timing: requested capture at 266.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,466 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bāwāli, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,467 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bāwāli, IN) window enabled: 214.2 to 266.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,467 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 266.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,471 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 266.0 for Target(Bāwāli, IN) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,472 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,473 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,551 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,578 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,581 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,581 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bagulā, IN) imaging timing: requested capture at 276.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,582 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bagulā, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,583 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bagulā, IN) window enabled: 184.7 to 286.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,584 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<266.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 286.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,594 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<286.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 286.0 for Target(Bagulā, IN) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,595 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<286.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,596 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<286.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,672 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<286.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,712 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<286.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,713 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<286.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,716 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<286.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Gobārdānga, IN) imaging timing: requested capture at 286.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,716 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<286.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Gobārdānga, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,718 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<286.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Gobārdānga, IN) window enabled: 196.6 to 287.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,719 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<286.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 287.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,721 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<288.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 287.6 for Target(Gobārdānga, IN) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,722 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<288.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,723 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<288.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,786 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<288.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,813 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<288.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,814 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<288.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,814 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<288.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Faradābād, BD) imaging timing: requested capture at 306.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,815 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<288.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Faradābād, BD) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,815 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<288.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Faradābād, BD) window enabled: 191.8 to 324.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,816 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<288.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 324.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,828 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<318.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Faradābād, BD)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,829 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<318.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.4171109179135972)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,830 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<318.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,895 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<318.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.4171109179135972)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,927 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<318.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,928 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<318.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 2 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,929 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<318.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Kakching, IN) imaging timing: requested capture at 333.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,929 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<318.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Kakching, IN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,930 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<318.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Kakching, IN) window enabled: 222.9 to 348.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,931 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<318.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 348.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,946 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<348.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 348.1 for Target(Kakching, IN) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,947 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<348.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:11,948 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<348.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,026 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<348.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,053 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<348.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,054 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<348.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,055 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<348.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Chandanais, BD) imaging timing: requested capture at 351.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,055 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<348.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Chandanais, BD) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,056 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<348.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Chandanais, BD) window enabled: 226.4 to 353.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,057 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<348.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 353.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,062 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<354.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 353.6 for Target(Chandanais, BD) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,063 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<354.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,063 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<354.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,124 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<354.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,152 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<354.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,154 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<354.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 2 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,155 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<354.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ban Pae, TH) imaging timing: requested capture at 420.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,155 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<354.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ban Pae, TH) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,156 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<354.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ban Pae, TH) window enabled: 349.6 to 486.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,157 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<354.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 486.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,182 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<420.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Ban Pae, TH)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,184 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<420.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.10437116422746319)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,184 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<420.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,252 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<420.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.10437116422746319)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,292 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<420.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,296 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<420.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,297 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<420.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ban San Phak Wan Luang, TH) imaging timing: requested capture at 452.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,298 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<420.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ban San Phak Wan Luang, TH) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,299 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<420.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ban San Phak Wan Luang, TH) window enabled: 348.2 to 484.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,299 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<420.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 484.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,306 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(San Sebastián de la Gomera, ES)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,308 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.795573591049628)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,308 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,312 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mFinding opportunity windows from 4200.00 to 4800.00 seconds\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,359 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mFinding opportunity windows from 4200.00 to 4800.00 seconds\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,418 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.795573591049628)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,458 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,460 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,460 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Machico, PT) imaging timing: requested capture at 488.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,461 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Machico, PT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,462 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Machico, PT) window enabled: 420.9 to 546.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,462 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<430.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 546.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,470 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<452.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Ban San Phak Wan Luang, TH)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,472 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<452.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.19947173638520796)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,473 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<452.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,543 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<452.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.19947173638520796)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,572 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<452.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,573 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<452.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 4 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,574 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<452.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ratchaburi, TH) imaging timing: requested capture at 496.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,574 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<452.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ratchaburi, TH) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,575 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<452.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ratchaburi, TH) window enabled: 440.2 to 541.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,576 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<452.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 541.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,589 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<488.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Machico, PT)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,591 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<488.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.38370711429960774)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,591 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<488.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,656 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<488.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.38370711429960774)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,685 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<488.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,699 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<488.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,703 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<488.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(San Sebastián de la Gomera, ES) imaging timing: requested capture at 488.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,704 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<488.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(San Sebastián de la Gomera, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,709 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<488.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(San Sebastián de la Gomera, ES) window enabled: 371.5 to 489.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,714 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<488.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 489.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,718 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<490.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 489.9 for Target(San Sebastián de la Gomera, ES) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,719 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<490.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,719 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<490.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,798 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<490.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,839 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<490.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,841 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<490.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,842 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<490.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Amdel, MA) imaging timing: requested capture at 569.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,842 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<490.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Amdel, MA) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,843 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<490.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Amdel, MA) window enabled: 529.1 to 610.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,845 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<490.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 610.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,852 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<501.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Ratchaburi, TH)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,853 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<501.50> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.019042621930721183)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,853 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<501.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,865 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<501.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mFinding opportunity windows from 4800.00 to 5400.00 seconds\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:12,971 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<501.50> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.019042621930721183)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,011 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<501.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,012 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<501.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 3 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,013 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<501.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Samrong, TH) imaging timing: requested capture at 529.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,014 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<501.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Samrong, TH) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,014 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<501.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Samrong, TH) window enabled: 440.3 to 557.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,015 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<501.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 557.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,025 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<529.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Samrong, TH)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,026 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<529.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.3456253471201201)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,027 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<529.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,127 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<529.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.3456253471201201)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,159 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<529.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,161 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<529.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 2 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,162 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<529.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ban Bang Lamung, TH) imaging timing: requested capture at 547.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,163 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<529.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ban Bang Lamung, TH) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,164 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<529.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Ban Bang Lamung, TH) window enabled: 453.9 to 566.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,164 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<529.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 566.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,171 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<547.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Ban Bang Lamung, TH)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,172 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<547.50> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.1966497718953449)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,173 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<547.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,248 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<547.50> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.1966497718953449)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,276 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<547.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,277 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<547.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,278 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<547.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Koh Kong, KH) imaging timing: requested capture at 580.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,278 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<547.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Koh Kong, KH) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,279 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<547.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Koh Kong, KH) window enabled: 492.1 to 614.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,280 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<547.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 614.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,310 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<569.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Amdel, MA)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,311 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<569.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.7745063964760883)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,312 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<569.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,395 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<569.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.7745063964760883)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,435 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<569.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,451 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<569.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,456 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<569.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(’Ali Ben Sliman, MA) imaging timing: requested capture at 595.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,460 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<569.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(’Ali Ben Sliman, MA) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,464 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<569.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(’Ali Ben Sliman, MA) window enabled: 567.9 to 621.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,465 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<569.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 621.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,471 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<583.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Koh Kong, KH)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,473 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<583.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.00857099491719181)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,474 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<583.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,569 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<583.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.00857099491719181)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,606 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<583.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,606 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<583.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 3 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,607 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<583.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Cần Thơ, VN) imaging timing: requested capture at 628.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,608 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<583.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Cần Thơ, VN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,609 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<583.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Cần Thơ, VN) window enabled: 540.1 to 673.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,610 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<583.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 673.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,618 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<595.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(’Ali Ben Sliman, MA)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,620 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<595.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.48662061623566244)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,621 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<595.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,733 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<595.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.48662061623566244)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,771 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<595.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,773 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<595.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,774 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<595.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Oulad Yaïch, MA) imaging timing: requested capture at 617.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,774 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<595.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Oulad Yaïch, MA) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,775 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<595.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Oulad Yaïch, MA) window enabled: 581.5 to 639.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,776 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<595.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 639.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,786 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<617.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Oulad Yaïch, MA)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,789 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<617.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.6838681854948269)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,789 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<617.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,873 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<617.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.6838681854948269)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,901 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<617.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,902 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<617.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 4 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,903 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<617.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Ain Kansara, MA) imaging timing: requested capture at 656.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,904 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<617.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Ain Kansara, MA) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,905 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<617.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Ain Kansara, MA) window enabled: 594.7 to 695.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,905 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<617.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 695.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,913 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<628.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Cần Thơ, VN)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,915 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<628.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.3663438367949623)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,918 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<628.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:13,985 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<628.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.3663438367949623)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,011 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<628.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,012 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<628.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,032 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<628.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Rạch Giá, VN) imaging timing: requested capture at 645.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,036 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<628.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Rạch Giá, VN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,038 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<628.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Rạch Giá, VN) window enabled: 534.7 to 662.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,039 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<628.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 662.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,045 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<645.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Rạch Giá, VN)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,047 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<645.00> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.6520300383357149)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,048 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<645.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,134 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<645.00> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.6520300383357149)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,175 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<645.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,176 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<645.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,177 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<645.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bình Minh, VN) imaging timing: requested capture at 659.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,177 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<645.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bình Minh, VN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,178 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<645.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Bình Minh, VN) window enabled: 539.2 to 673.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,179 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<645.00> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 673.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,184 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<656.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Ain Kansara, MA)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,186 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<656.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.5837788834463559)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,186 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<656.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,258 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<656.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.5837788834463559)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,305 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<656.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,307 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<656.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 7 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,313 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<656.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Algeciras, ES) imaging timing: requested capture at 688.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,314 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<656.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Algeciras, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,316 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<656.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Algeciras, ES) window enabled: 586.9 to 721.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,316 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<656.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 721.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,319 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<658.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mimaged Target(Bình Minh, VN)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,321 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<658.50> \u001b[0m\u001b[mTotal reward: {'EO_0': np.float64(0.6409562025459951)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,321 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<658.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,390 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<658.50> \u001b[0m\u001b[mStep reward: {'EO_0': np.float64(0.6409562025459951)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,417 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<658.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,418 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<658.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,419 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<658.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Long Thành, VN) imaging timing: requested capture at 667.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,420 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<658.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Long Thành, VN) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,421 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<658.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Long Thành, VN) window enabled: 541.8 to 677.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,421 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<658.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 677.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,431 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<677.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtimed termination at 677.3 for Target(Long Thành, VN) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,432 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<677.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,433 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<677.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mSatellite EO_0 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,509 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<677.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,548 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<677.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,549 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<677.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 20 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,551 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<677.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Tena, CO) imaging timing: requested capture at 3964.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,552 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<677.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Tena, CO) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,553 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<677.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Tena, CO) window enabled: 3901.7 to 4027.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,554 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<677.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 4027.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,559 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<688.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Algeciras, ES)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,561 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<688.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.025730269188953946)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,562 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<688.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,661 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<688.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.025730269188953946)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,703 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<688.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,706 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<688.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,706 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<688.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Ain Kansara, MA) imaging timing: requested capture at 692.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,707 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<688.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Ain Kansara, MA) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,708 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<688.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Ain Kansara, MA) window enabled: 594.7 to 695.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,708 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<688.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 695.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,712 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<696.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 695.7 for Target(Ain Kansara, MA) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,713 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<696.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,715 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<696.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,804 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<696.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,846 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<696.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,847 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<696.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,848 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<696.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Teroual, MA) imaging timing: requested capture at 699.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,848 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<696.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Teroual, MA) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,849 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<696.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Teroual, MA) window enabled: 585.4 to 702.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,849 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<696.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 702.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,854 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<701.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Teroual, MA)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,854 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<701.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.07431902323793338)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,854 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<701.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,945 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<701.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.07431902323793338)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,991 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<701.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,992 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<701.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 2 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,992 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<701.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Dar El Kebdani, MA) imaging timing: requested capture at 714.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,993 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<701.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Dar El Kebdani, MA) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:14,996 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<701.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Dar El Kebdani, MA) window enabled: 618.3 to 727.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,000 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<701.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 727.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,030 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<725.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Dar El Kebdani, MA)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,031 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<725.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.046522390704619956)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,033 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<725.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,135 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<725.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.046522390704619956)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,165 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<725.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,167 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<725.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 5 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,169 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<725.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Atarfe, ES) imaging timing: requested capture at 739.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,170 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<725.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Atarfe, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,171 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<725.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Atarfe, ES) window enabled: 617.5 to 753.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,171 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<725.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 753.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,188 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 753.7 for Target(Atarfe, ES) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,190 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,190 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,272 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,304 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,307 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,308 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Maracena, ES) imaging timing: requested capture at 754.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,309 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Maracena, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,311 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Maracena, ES) window enabled: 618.1 to 754.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,312 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 754.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,314 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 754.2 for Target(Maracena, ES) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,315 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,315 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,424 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,460 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,462 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 4 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,464 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Alhama de Murcia, ES) imaging timing: requested capture at 770.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,465 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Alhama de Murcia, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,466 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Alhama de Murcia, ES) window enabled: 651.4 to 786.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,466 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<754.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 786.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,468 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<755.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Alhama de Murcia, ES)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,470 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<755.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.002593261933429352)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,471 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<755.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,558 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<755.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.002593261933429352)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,590 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<755.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,592 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<755.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,593 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<755.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Almería, ES) imaging timing: requested capture at 759.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,594 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<755.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Almería, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,595 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<755.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Almería, ES) window enabled: 632.4 to 763.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,596 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<755.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 763.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,602 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<763.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 763.1 for Target(Almería, ES) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,603 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<763.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,604 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<763.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,703 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<763.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,739 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<763.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,741 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<763.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,742 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<763.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Villarrobledo, ES) imaging timing: requested capture at 772.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,744 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<763.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Villarrobledo, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,745 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<763.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Villarrobledo, ES) window enabled: 650.0 to 781.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,745 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<763.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 781.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,752 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<782.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 781.6 for Target(Villarrobledo, ES) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,753 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<782.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,754 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<782.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,836 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<782.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,873 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<782.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,875 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<782.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 4 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,876 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<782.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Valencia, ES) imaging timing: requested capture at 797.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,876 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<782.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Valencia, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,882 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<782.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Valencia, ES) window enabled: 676.7 to 812.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,894 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<782.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 812.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,915 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<810.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Valencia, ES)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,917 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<810.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.060854626884254755)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:15,918 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<810.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,004 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<810.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.060854626884254755)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,042 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<810.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,044 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<810.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,046 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<810.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Benifayó, ES) imaging timing: requested capture at 811.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,048 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<810.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Benifayó, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,051 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<810.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Benifayó, ES) window enabled: 674.3 to 810.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,065 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<810.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 810.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,071 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 810.6 for Target(Benifayó, ES) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,074 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,075 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,165 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,198 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,200 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 4 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,201 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Binisalem, ES) imaging timing: requested capture at 832.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,201 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Binisalem, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,202 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Binisalem, ES) window enabled: 717.7 to 853.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,202 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 853.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,205 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Binisalem, ES)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,207 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.8723150559752924)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,207 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,287 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.8723150559752924)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,326 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,328 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 3 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,329 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Calviá, ES) imaging timing: requested capture at 830.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,331 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Calviá, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,332 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Calviá, ES) window enabled: 712.8 to 848.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,333 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<811.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 848.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,347 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<843.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Calviá, ES)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,349 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<843.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.06112811158338784)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,351 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<843.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,418 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<843.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.06112811158338784)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,506 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<843.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,508 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<843.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,509 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<843.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Binisalem, ES) imaging timing: requested capture at 848.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,510 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<843.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Binisalem, ES) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,511 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<843.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Binisalem, ES) window enabled: 717.7 to 853.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,511 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<843.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 853.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,517 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<852.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Binisalem, ES)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,519 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<852.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,520 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<852.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,595 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<852.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,639 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<852.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,641 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<852.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 9 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,643 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<852.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Noceto, IT) imaging timing: requested capture at 907.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,644 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<852.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Noceto, IT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,645 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<852.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Noceto, IT) window enabled: 855.4 to 959.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,645 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<852.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 959.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,664 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<907.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Noceto, IT)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,666 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<907.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.7179888850051652)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,666 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<907.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,772 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<907.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.7179888850051652)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,817 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<907.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,818 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<907.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,819 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<907.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Volvera, IT) imaging timing: requested capture at 910.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,820 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<907.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Volvera, IT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,821 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<907.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Volvera, IT) window enabled: 839.4 to 913.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,821 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<907.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 913.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,828 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<914.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 913.7 for Target(Volvera, IT) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,830 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<914.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,834 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<914.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,919 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<914.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,958 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<914.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,960 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<914.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,960 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<914.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Terralba, IT) imaging timing: requested capture at 916.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,962 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<914.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Terralba, IT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,964 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<914.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Terralba, IT) window enabled: 800.6 to 918.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,964 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<914.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 918.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,970 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<919.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 918.6 for Target(Terralba, IT) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,971 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<919.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:16,972 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<919.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,055 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<919.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,098 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<919.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,100 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<919.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,100 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<919.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Gallarate, IT) imaging timing: requested capture at 919.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,101 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<919.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Gallarate, IT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,102 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<919.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Gallarate, IT) window enabled: 871.6 to 920.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,103 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<919.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 920.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,106 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<920.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 920.0 for Target(Gallarate, IT) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,107 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<920.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,108 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<920.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,187 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<920.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,219 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<920.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,220 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<920.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 5 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,221 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<920.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Castelfranco di Sotto, IT) imaging timing: requested capture at 946.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,222 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<920.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Castelfranco di Sotto, IT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,222 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<920.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Castelfranco di Sotto, IT) window enabled: 844.1 to 972.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,223 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<920.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 972.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,233 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<946.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Castelfranco di Sotto, IT)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,235 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<946.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.11166066216117564)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,236 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<946.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,322 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<946.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.11166066216117564)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,354 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<946.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,355 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<946.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,356 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<946.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(La Spezia, IT) imaging timing: requested capture at 952.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,356 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<946.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(La Spezia, IT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,357 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<946.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(La Spezia, IT) window enabled: 839.7 to 958.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,358 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<946.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 958.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,365 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<955.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(La Spezia, IT)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,366 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<955.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.5782403085024662)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,367 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<955.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,452 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<955.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.5782403085024662)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,485 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<955.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,487 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<955.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,488 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<955.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Santa Croce sull’ Arno, IT) imaging timing: requested capture at 963.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,488 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<955.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Santa Croce sull’ Arno, IT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,489 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<955.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Santa Croce sull’ Arno, IT) window enabled: 844.5 to 972.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,490 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<955.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 972.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,495 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<964.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Santa Croce sull’ Arno, IT)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,496 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<964.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.6116193943991023)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,497 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<964.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,579 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<964.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.6116193943991023)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,608 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<964.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,610 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<964.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,611 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<964.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Castelvetro di Modena, IT) imaging timing: requested capture at 968.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,612 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<964.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Castelvetro di Modena, IT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,615 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<964.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Castelvetro di Modena, IT) window enabled: 856.9 to 972.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,616 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<964.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 972.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,624 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<973.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 972.9 for Target(Castelvetro di Modena, IT) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,625 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<973.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,626 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<973.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,703 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<973.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,742 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<973.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,744 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<973.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 6 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,745 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<973.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Marano di Napoli, IT) imaging timing: requested capture at 984.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,746 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<973.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Marano di Napoli, IT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,747 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<973.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Marano di Napoli, IT) window enabled: 879.8 to 995.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,748 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<973.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 995.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,757 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<995.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 995.1 for Target(Marano di Napoli, IT) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,759 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<995.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,759 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<995.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,852 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<995.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,896 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<995.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,898 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<995.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,899 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<995.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Potenza, IT) imaging timing: requested capture at 1001.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,899 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<995.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Potenza, IT) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,901 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<995.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Potenza, IT) window enabled: 907.4 to 1007.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,902 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<995.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1007.2\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,910 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1007.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 1007.2 for Target(Potenza, IT) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,912 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1007.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,913 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1007.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:17,982 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1007.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,018 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1007.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,020 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1007.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 5 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,021 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1007.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Rogatec, SI) imaging timing: requested capture at 1017.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,021 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1007.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Rogatec, SI) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,022 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1007.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Rogatec, SI) window enabled: 932.6 to 1026.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,023 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1007.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1026.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,036 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1027.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 1026.7 for Target(Rogatec, SI) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,037 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1027.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,038 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1027.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,078 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1027.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mFinding opportunity windows from 4800.00 to 5400.00 seconds\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,151 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1027.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,191 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1027.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,193 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1027.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 10 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,194 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1027.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Ljubovija, RS) imaging timing: requested capture at 1055.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,195 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1027.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Ljubovija, RS) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,196 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1027.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Ljubovija, RS) window enabled: 948.3 to 1084.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,197 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1027.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1084.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,209 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1062.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Ljubovija, RS)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,213 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1062.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.005514410220247971)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,216 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1062.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,295 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1062.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.005514410220247971)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,334 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1062.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,336 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1062.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 13 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,338 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1062.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Rankovce, MK) imaging timing: requested capture at 1082.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,339 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1062.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Rankovce, MK) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,340 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1062.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Rankovce, MK) window enabled: 987.4 to 1102.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,340 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1062.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1102.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,352 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1099.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Rankovce, MK)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,353 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1099.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.023765420073078768)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,354 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1099.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,429 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1099.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.023765420073078768)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,460 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1099.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,462 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1099.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 10 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,463 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1099.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Covasna, RO) imaging timing: requested capture at 1134.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,465 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1099.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Covasna, RO) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,466 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1099.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Covasna, RO) window enabled: 1039.9 to 1169.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,467 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1099.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1169.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,487 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1145.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Covasna, RO)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,489 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1145.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.0030979265085962784)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,489 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1145.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,555 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1145.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.0030979265085962784)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,590 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1145.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,592 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1145.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,593 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1145.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Boldeşti-Scăeni, RO) imaging timing: requested capture at 1157.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,593 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1145.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Boldeşti-Scăeni, RO) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,594 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1145.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Boldeşti-Scăeni, RO) window enabled: 1033.4 to 1169.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,597 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1145.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1169.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,604 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1157.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Boldeşti-Scăeni, RO)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,606 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1157.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.2270585561858818)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,607 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1157.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,685 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1157.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.2270585561858818)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,721 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1157.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,722 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1157.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 2 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,723 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1157.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Bilozerka, UA) imaging timing: requested capture at 1199.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,724 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1157.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Bilozerka, UA) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,724 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1157.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Bilozerka, UA) window enabled: 1120.7 to 1242.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,725 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1157.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1242.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,744 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Bilozerka, UA)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,746 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.10296013415656079)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,746 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,814 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.10296013415656079)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,880 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,881 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mtarget index 17 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,882 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Aquitania, CO) imaging timing: requested capture at 3995.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,882 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Aquitania, CO) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,884 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[mTarget(Aquitania, CO) window enabled: 3929.0 to 4061.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,884 \u001b[0m\u001b[36msats.satellite.EO_0 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[36mEO_0: \u001b[0m\u001b[msetting timed terminal event at 4061.5\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,885 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,886 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Zelenodolsk, UA) imaging timing: requested capture at 1224.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,887 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Zelenodolsk, UA) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,887 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Zelenodolsk, UA) window enabled: 1146.8 to 1245.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,888 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1203.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1245.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,899 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1228.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Zelenodolsk, UA)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,901 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1228.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.525948564551845)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,902 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1228.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:18,971 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1228.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.525948564551845)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,002 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1228.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,004 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1228.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,004 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1228.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Pokrovsk, UA) imaging timing: requested capture at 1248.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,005 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1228.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Pokrovsk, UA) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,006 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1228.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Pokrovsk, UA) window enabled: 1206.6 to 1268.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,006 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1228.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1268.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,023 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1263.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Pokrovsk, UA)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,026 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1263.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.38900071434681843)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,026 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1263.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,105 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1263.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.38900071434681843)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,140 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1263.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,142 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1263.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,143 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1263.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Yuvileine, UA) imaging timing: requested capture at 1267.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,143 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1263.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Yuvileine, UA) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,144 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1263.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Yuvileine, UA) window enabled: 1248.0 to 1272.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,145 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1263.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1272.4\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,151 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1272.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 1272.4 for Target(Yuvileine, UA) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,152 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1272.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,153 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1272.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,223 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1272.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,258 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1272.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,261 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1272.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,262 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1272.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Sokhumi, GE) imaging timing: requested capture at 1313.0\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,263 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1272.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Sokhumi, GE) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,263 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1272.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Sokhumi, GE) window enabled: 1229.6 to 1353.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,264 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1272.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1353.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,283 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1323.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Sokhumi, GE)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,285 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1323.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.20551218533976295)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,288 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1323.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,348 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1323.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.20551218533976295)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,381 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1323.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,382 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1323.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 1 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,383 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1323.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Saatlı, AZ) imaging timing: requested capture at 1396.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,384 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1323.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Saatlı, AZ) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,385 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1323.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Saatlı, AZ) window enabled: 1384.9 to 1408.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,385 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1323.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1408.3\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,416 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1396.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Saatlı, AZ)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,419 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1396.50> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.7923814938276732)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,419 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1396.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,495 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1396.50> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.7923814938276732)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,520 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1396.50> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,522 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1396.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 2 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,523 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1396.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Corat, AZ) imaging timing: requested capture at 1426.1\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,523 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1396.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Corat, AZ) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,524 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1396.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Corat, AZ) window enabled: 1368.1 to 1455.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,525 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1396.50> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1455.7\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,535 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1426.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Corat, AZ)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,537 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1426.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.8375055149095487)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,538 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1426.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,614 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1426.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.8375055149095487)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,648 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1426.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,650 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1426.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,651 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1426.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Xudat, AZ) imaging timing: requested capture at 1438.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,651 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1426.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Xudat, AZ) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,652 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1426.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Xudat, AZ) window enabled: 1337.2 to 1451.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,652 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1426.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1451.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,660 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1448.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mimaged Target(Xudat, AZ)\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,663 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1448.00> \u001b[0m\u001b[mTotal reward: {'EO_1': np.float64(0.7590256629768871)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,664 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1448.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,735 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1448.00> \u001b[0m\u001b[mStep reward: {'EO_1': np.float64(0.7590256629768871)}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,770 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1448.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,773 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1448.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 0 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,777 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1448.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Əmircan, AZ) imaging timing: requested capture at 1452.9\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,778 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1448.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Əmircan, AZ) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,779 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1448.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Əmircan, AZ) window enabled: 1374.4 to 1457.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,779 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1448.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1457.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,784 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1458.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtimed termination at 1457.8 for Target(Əmircan, AZ) window\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,789 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1458.00> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,789 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1458.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mSatellite EO_1 requires retasking\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,865 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1458.00> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,901 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1458.00> \u001b[0m\u001b[93;1m=== STARTING STEP ===\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,905 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1458.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mtarget index 2 tasked\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,908 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1458.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Gurlan, UZ) imaging timing: requested capture at 1541.6\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,908 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1458.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Gurlan, UZ) tasked for imaging\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,910 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1458.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[mTarget(Gurlan, UZ) window enabled: 1474.4 to 1608.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,910 \u001b[0m\u001b[92msats.satellite.EO_1 \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1458.00> \u001b[0m\u001b[92mEO_1: \u001b[0m\u001b[msetting timed terminal event at 1608.8\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:19,934 \u001b[0m\u001b[mdata.base \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1513.50> \u001b[0m\u001b[mTotal reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:20,005 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1513.50> \u001b[0m\u001b[mStep reward: {}\u001b[0m\n", + "\u001b[90;3m2026-10-26 21:02:20,006 \u001b[0m\u001b[mgym \u001b[0m\u001b[mINFO \u001b[0m\u001b[33m<1513.50> \u001b[0m\u001b[mEpisode truncated: ['EO_0', 'EO_1']\u001b[0m\n" + ] + } + ], + "source": [ + "while True:\n", + " actions = {}\n", + " current_time = env.simulator.sim_time\n", + "\n", + " old_intents = copy(intents)\n", + " intents = {}\n", + "\n", + " intents_quality_old = copy(intents_quality)\n", + " intents_quality = {}\n", + "\n", + " imaged_set = env.rewarder.data.imaged\n", + "\n", + " satellites_needing_retask = []\n", + " for satellite in env.satellites:\n", + " needs_retask = (\n", + " satellite.name in info and info[satellite.name][\"requires_retasking\"]\n", + " )\n", + " exceeded_time_limit = (\n", + " current_time - sat_last_decision_time[satellite.name] >= 500.0\n", + " )\n", + " intent_imaged = (\n", + " old_intents[satellite.name] in imaged_set\n", + " if old_intents[satellite.name] is not None\n", + " else False\n", + " ) and comm_mode not in [\"incremental_intent\", \"greedy_incremental\"]\n", + "\n", + " if needs_retask or exceeded_time_limit or intent_imaged:\n", + " satellites_needing_retask.append(satellite.name)\n", + " intents[satellite.name] = None\n", + " intents_quality[satellite.name] = None\n", + " else:\n", + " intents[satellite.name] = old_intents[satellite.name]\n", + " intents_quality[satellite.name] = intents_quality_old[satellite.name]\n", + "\n", + " while len(satellites_needing_retask) > 0:\n", + " for satellite in env.satellites:\n", + " if satellite.name in satellites_needing_retask:\n", + " if comm_mode == \"imaged\":\n", + " satellite.imaged_set_shared = imaged_set\n", + "\n", + " if comm_mode == \"intent\":\n", + " satellite.imaged_set_shared = imaged_set\n", + " satellite.intent_set_shared = set(intents.values()) - {None}\n", + "\n", + " if comm_mode == \"incremental_intent\":\n", + " satellite.bookkeeping.delete_intended()\n", + " for tgt in env.rewarder.data.imaged:\n", + " satellite.bookkeeping.update(\n", + " tgt.id, tgt.highest_reward_obtained, current_time\n", + " )\n", + " for intent_quality in intents_quality.values():\n", + " if intent_quality is not None:\n", + " satellite.bookkeeping.update_intended(\n", + " intent_quality[0].id,\n", + " intent_quality[1],\n", + " intent_quality[2],\n", + " )\n", + "\n", + " if comm_mode == \"greedy\":\n", + " satellite.temp_filter_set = set()\n", + " satellite.imaged_set_shared = imaged_set\n", + "\n", + " if comm_mode == \"greedy_incremental\":\n", + " satellite.temp_filter_set = set()\n", + " satellite.bookkeeping.delete_intended()\n", + " for tgt in env.rewarder.data.imaged:\n", + " satellite.bookkeeping.update(\n", + " tgt.id, tgt.highest_reward_obtained, current_time\n", + " )\n", + "\n", + " satellite.observation_builder.obs_dict_cache = (\n", + " None # Clear cache to force re-evaluation of the observation\n", + " )\n", + "\n", + " sat_last_decision_time[satellite.name] = current_time\n", + " action = policy(satellite)\n", + " imaging_action = get_imaging_action(env, action)\n", + " if (\n", + " comm_mode in [\"greedy\", \"greedy_incremental\"]\n", + " and imaging_action is not None\n", + " ):\n", + " target_index, action_fraction = imaging_action\n", + " target_parsed = satellite.parse_target_selection(target_index)\n", + "\n", + " while target_parsed in intents.values():\n", + " expected_absolute_reward = get_expected_absolute_reward(\n", + " env, satellite, target_index, action_fraction\n", + " )\n", + "\n", + " intent_rewards = {\n", + " val[0].id: (key, val[1])\n", + " for key, val in intents_quality.items()\n", + " if val is not None\n", + " }\n", + "\n", + " prev_data = intent_rewards.get(target_parsed.id)\n", + " prev_sat_name = prev_data[0]\n", + " prev_reward = prev_data[1]\n", + "\n", + " if expected_absolute_reward > prev_reward:\n", + " # Assigns the new satellite's intent since it has a better expected reward than the previous satellite's intent\n", + " intents[satellite.name] = target_parsed\n", + " intents_quality[satellite.name] = (\n", + " target_parsed,\n", + " expected_absolute_reward,\n", + " get_time(env, action, satellite.name),\n", + " )\n", + "\n", + " # Removes old satellites intent if the new satellite has a better expected reward\n", + " intents[prev_sat_name] = None\n", + " intents_quality[prev_sat_name] = None\n", + " actions[prev_sat_name] = None\n", + " satellites_needing_retask.append(prev_sat_name)\n", + " break\n", + " else:\n", + " satellite.temp_filter_set.add(target_parsed)\n", + " satellite.observation_builder.obs_dict_cache = None # Clear cache to force re-evaluation of the observation\n", + " action = policy(satellite)\n", + "\n", + " imaging_action = get_imaging_action(env, action)\n", + " if imaging_action is None:\n", + " break\n", + " target_index, action_fraction = imaging_action\n", + " target_parsed = satellite.parse_target_selection(\n", + " target_index\n", + " )\n", + "\n", + " actions[satellite.name] = action\n", + " imaging_action = get_imaging_action(env, action)\n", + " if imaging_action is not None:\n", + " target_index, action_fraction = imaging_action\n", + " target_parsed = satellite.parse_target_selection(target_index)\n", + " intents[satellite.name] = target_parsed\n", + " if comm_mode in [\n", + " \"incremental_intent\",\n", + " \"greedy\",\n", + " \"greedy_incremental\",\n", + " ]:\n", + " expected_absolute_reward = get_expected_absolute_reward(\n", + " env, satellite, target_index, action_fraction\n", + " )\n", + " intents_quality[satellite.name] = (\n", + " target_parsed,\n", + " expected_absolute_reward,\n", + " get_time(env, action, satellite.name),\n", + " )\n", + "\n", + " satellites_needing_retask.remove(satellite.name)\n", + "\n", + " _, reward, terminated, truncated, info = env.step(actions) # type: ignore\n", + "\n", + " reward_cumulative += sum(reward.values())\n", + "\n", + " if any(terminated.values()) or any(truncated.values()):\n", + " break" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv_issue_363 (3.12.7)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From fc9242570fa3e6819a4027df3ae3782261363340 Mon Sep 17 00:00:00 2001 From: LorenzzoQM Date: Mon, 26 Oct 2026 21:06:53 -0600 Subject: [PATCH 4/4] [#363] Update release notes --- docs/source/release_notes.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst index 7d62bbde..af8aa696 100644 --- a/docs/source/release_notes.rst +++ b/docs/source/release_notes.rst @@ -5,6 +5,20 @@ Development - |version| ----------------------- .. *Release Date: MMM. DD, YYYY* +* Add optional hybrid action spaces with ``hybrid=True`` on supported discrete + actions. Mode, broadcast, and ground-imaging actions accept a requested duration. + Ground imaging instead uses a desired capture-time offset from the current + simulation time when either ``timeToleranceLower`` or ``timeToleranceUpper`` is + enabled. Configure these bounds in seconds; each defaults to zero, disabling + that bound. The imaging mode is selected on each reset, with a warning when + capture-time offsets are used. Action spaces remain purely discrete by default. + Mixed discrete/hybrid actions and grouped constellation actions are supported. + Actions with a requested duration run for at least one simulator time step, + even when a shorter duration is requested or configured. + See issue #363. +* Add :doc:`the hybrid imaging and image quality example `, + demonstrating timing wrappers, time-varying acquisition quality and rewards, + RLlib policy setup, and multi-satellite coordination. * Add ``block_pending_targets=False`` to :class:`~bsk_rl.scene.RSOTargets` to allow reimaging before downlink, preserving cooldowns and image/reward bookkeeping. Pending targets remain blocked by default. See issue #364.