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"]