Source code for bsk_rl.scene.targets

"""Target scenarios distribute ground targets with some distribution.

Currently, targets are all known to the satellites a priori and are available based on
the imaging requirements given by the dynamics and flight software models.
"""

import logging
import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Union

import numpy as np
import pandas as pd
from Basilisk.utilities import orbitalMotion

from bsk_rl.scene import Scenario
from bsk_rl.utils.orbital import lla2ecef

if TYPE_CHECKING:  # pragma: no cover
    from bsk_rl.data.base import Data
    from bsk_rl.sats import Satellite

logger = logging.getLogger(__name__)


[docs] class Target: """Ground target with associated value.""" def __init__(self, name: str, r_LP_P: Iterable[float], priority: float) -> None: """Ground target with associated priority and location. Args: name: Identifier; does not need to be unique r_LP_P: Planet-fixed, planet relative location [m] priority: Value metric. """ self.name = name self.r_LP_P = np.array(r_LP_P) self.priority = priority @property def id(self) -> str: """Get unique, human-readable identifier.""" try: return self._id except AttributeError: self._id = f"{self.name}_{id(self)}" return self._id def __hash__(self) -> int: """Hash target by unique id.""" return hash((self.id)) def __repr__(self) -> str: """Get string representation of target. Use ``target.id`` for a unique string identifier. Returns: Target string """ return f"Target({self.name})"
[docs] class UniformTargets(Scenario): """Environment with targets distributed uniformly.""" def __init__( self, n_targets: Union[int, tuple[int, int]], priority_distribution: Optional[Callable] = None, radius: float = orbitalMotion.REQ_EARTH * 1e3, ) -> None: """An environment with evenly-distributed static targets. Can be used with :class:`~bsk_rl.data.UniqueImageReward`. Args: n_targets: Number of targets to generate. Can also be specified as a range ``(low, high)`` where the number of targets generated is uniformly selected ``low ≤ n_targets ≤ high``. priority_distribution: Function for generating target priority. Defaults to ``lambda: uniform(0, 1)`` if not specified. radius: [m] Radius to place targets from body center. Defaults to Earth's equatorial radius. """ self._n_targets = n_targets if priority_distribution is None: priority_distribution = lambda: np.random.rand() # noqa: E731 self.priority_distribution = priority_distribution self.radius = radius
[docs] def reset_overwrite_previous(self) -> None: """Overwrite target list from previous episode.""" self.targets = []
[docs] def reset_pre_sim_init(self) -> None: """Regenerate target set for new episode.""" if isinstance(self._n_targets, int): self.n_targets = self._n_targets else: self.n_targets = np.random.randint(self._n_targets[0], self._n_targets[1]) logger.info(f"Generating {self.n_targets} targets") self.regenerate_targets() for satellite in self.satellites: if hasattr(satellite, "add_location_for_access_checking"): for target in self.targets: satellite.add_location_for_access_checking( object=target, r_LP_P=target.r_LP_P, min_elev=satellite.sat_args_generator[ "imageTargetMinimumElevation" ], # Assume not randomized type="target", )
[docs] def regenerate_targets(self) -> None: """Regenerate targets uniformly. Override this method (as demonstrated in :class:`CityTargets`) to generate other distributions. """ self.targets = [] for i in range(self.n_targets): x = np.random.normal(size=3) x *= self.radius / np.linalg.norm(x) self.targets.append( Target(name=f"tgt-{i}", r_LP_P=x, priority=self.priority_distribution()) )
[docs] class CityTargets(UniformTargets): """Environment with targets distributed around population centers.""" def __init__( self, n_targets: Union[int, tuple[int, int]], n_select_from: Optional[int] = None, location_offset: float = 0, priority_distribution: Optional[Callable] = None, radius: float = orbitalMotion.REQ_EARTH * 1e3, ) -> None: """Construct environment with static targets around population centers. Uses the `simplemaps Word Cities Database <https://simplemaps.com/data/world-cities>`_ for population center locations. This data is installed by ``finish_install``. Args: n_targets: Number of targets to generate, as a fixed number or a range. n_select_from: Generate targets from the top `n_select_from` most populous cities. Will use all cities in the database if not specified. location_offset: [m] Offset targets randomly from the city center by up to this amount. priority_distribution: Function for generating target priority. radius: Radius to place targets from body center. """ super().__init__(n_targets, priority_distribution, radius) if n_select_from == "all" or n_select_from is None: n_select_from = sys.maxsize self.n_select_from = n_select_from self.location_offset = location_offset def regenerate_targets(self) -> None: """Regenerate targets based on cities. :meta private: """ self.targets = [] cities = pd.read_csv( Path(os.path.realpath(__file__)).parent.parent / "_dat" / "simplemaps_worldcities" / "worldcities.csv", ) if self.n_select_from > len(cities): self.n_select_from = len(cities) for i in np.random.choice(self.n_select_from, self.n_targets, replace=False): city = cities.iloc[i] location = lla2ecef(city["lat"], city["lng"], self.radius) offset = np.random.normal(size=3) offset /= np.linalg.norm(offset) offset *= self.location_offset location += offset location /= np.linalg.norm(location) location *= self.radius self.targets.append( Target( name=city["city"].replace("'", ""), r_LP_P=location, priority=self.priority_distribution(), ) )
__doc_title__ = "Target Scenarios" __all__ = ["Target", "UniformTargets", "CityTargets"]