From a346f6b4d5c6bbd66d5a4a1cf1ed6a488399c3f0 Mon Sep 17 00:00:00 2001 From: Yumeka Nagano <> Date: Thu, 18 Dec 2025 11:24:04 -0700 Subject: [PATCH 1/2] [#325] Add target location uncertainty model example --- examples/target_location_uncertainty.ipynb | 574 +++++++++++++++++++++ 1 file changed, 574 insertions(+) create mode 100644 examples/target_location_uncertainty.ipynb diff --git a/examples/target_location_uncertainty.ipynb b/examples/target_location_uncertainty.ipynb new file mode 100644 index 00000000..0f9a95e2 --- /dev/null +++ b/examples/target_location_uncertainty.ipynb @@ -0,0 +1,574 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Target Location Uncertainty\n", + "This tutorial demonstrates how to model uncertainty in target locations when imaging ground targets using a simple single-satellite BSK-RL environment. It also shows how target location uncertainty can grow over time when a target is not imaged.\n", + "\n", + "## Loading Modules" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from typing import Optional, Iterable, Any\n", + "\n", + "from Basilisk.architecture import bskLogging\n", + "from Basilisk.utilities import orbitalMotion, macros\n", + "from bsk_rl import act, obs, sats\n", + "from bsk_rl.sim import dyn, fsw, world\n", + "from bsk_rl.sim.fsw import action\n", + "from bsk_rl.scene.targets import UniformTargets\n", + "from bsk_rl.data.unique_image_data import UniqueImageData, UniqueImageReward\n", + "from bsk_rl.utils.orbital import random_orbit\n", + "from bsk_rl.gym import SatelliteTasking\n", + "\n", + "bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Making a Scenario with Location Uncertain Targets\n", + "\n", + "To account for location uncertainty in the simulation process, the following parameters are assigned to each target using UniformTargets as a base:\n", + "\n", + "* `search_area` represents a square region on the surface within which the estimated location is the center of the area.\n", + "\n", + "* `acquiring_speed` represents the apparent ground-track speed, sampled uniformly to reflect orbital geometry and latitude effects.\n", + "\n", + "* `scan_width` represents the fixed width of the imaging swath of the satellite.\n", + "\n", + "* `search_duration` represents the total imaging duration, proportional to the number of sensor swaths required to cover the area.\n", + "\n", + "* `image_time` represents the true imaging time within the duration window, drawn from a Gaussian distribution centered at the mid-point of $T_i$ ($\\mu_i$) with standard deviation $0.3T_i$ ($\\sigma_i$), truncated to nonnegative values. Approximately \\(4.8\\%\\) of samples occur after the window ends.\n", + "\n", + "* `speed` represents the target's ground speed, sampled uniformly to reflect typical ship speeds and fixed for each target." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class AreaTargets(UniformTargets):\n", + "\n", + " def regenerate_targets(self) -> None:\n", + " super().regenerate_targets()\n", + " for target in self.targets:\n", + " side_length = np.random.uniform(10.0, 100.0) * 1e3 # m\n", + " target.search_area = side_length**2 # m^2\n", + " target.acquiring_speed = np.random.uniform(3.0, 6.0) * 1e3 # m/s\n", + " scan_width = 20.0 * 1e3 # m\n", + " search_duration = (\n", + " side_length * np.ceil(side_length / scan_width) / target.acquiring_speed\n", + " )\n", + " target.search_duration = search_duration # s\n", + "\n", + " mean_duration = search_duration / 2.0\n", + " std_duration = search_duration * 0.3\n", + " image_time = np.random.normal(loc=mean_duration, scale=std_duration)\n", + " image_time = max(0.0, image_time)\n", + " target.image_time = image_time # s\n", + "\n", + " target.speed = np.random.uniform(22e3 / 3600, 46e3 / 3600) # m/s" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Making a Rewarder Considering Target Uncertainty\n", + "\n", + "When targets have location uncertainty, imaging the highest-priority target is not always the optimal choice. If a target is not imaged for some time, its uncertainty grows, which can reduce future imaging effectiveness. Therefore, the reward function should balance target priority and uncertainty. One way to achieve this is by multiplying the priority by the uncertainty for each target.\n", + "The `UncertainMultiplyReward` is built on the [UniqueImageReward](../api_reference/data/index.rst) base class and defines the reward function as\n", + "\n", + "$$\n", + "R(s, a, s') = \n", + "\\begin{cases}\n", + "p_n \\cdot u_n, & \\text{if } a = a_{\\text{image}, n} \\text{ and } t_{\\text{search}} = t_n^{\\ast} \\\\\n", + "0, & \\text{otherwise}\n", + "\\end{cases}\n", + "$$\n", + "\n", + "where $p_n$ is the target priority, $u_n$ is the target uncertainty (defined as the search area normalized by the maximum initial search area of 100~km $\\times$ 100~km), $t_{\\text{search}}$ is the time spent searching the target, $t_n^{\\ast}$ is the true imaging duration, and $a_{\\text{image}, n}$ denotes the imaging action for the selected target $n$.\n", + "Unlike `UniqueImageReward`, this class removes the imaging list filter, allowing the same target to be imaged multiple times." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TYPE_CHECKING\n", + "\n", + "if TYPE_CHECKING: # pragma: no cover\n", + " from bsk_rl.sats import Satellite\n", + "\n", + "\n", + "class UncertainMultiplyReward(UniqueImageReward):\n", + "\n", + " def create_data_store(self, satellite: \"Satellite\") -> None:\n", + " \"\"\"Create a data store for a satellite.\n", + "\n", + " Args:\n", + " satellite: Satellite to create a data store for.\n", + " \"\"\"\n", + " satellite.data_store = self.data_store_type(\n", + " satellite,\n", + " initial_data=self.initial_data(satellite),\n", + " **self.data_store_kwargs,\n", + " )\n", + " self.cum_reward[satellite.name] = 0.0\n", + "\n", + " def calculate_reward(\n", + " self, new_data_dict: dict[str, UniqueImageData]\n", + " ) -> dict[str, float]:\n", + " \"\"\"Reward images of targets based on their priority and current uncertainty.\n", + "\n", + " Targets may be imaged multiple times; each image earns reward based on\n", + " ``self.reward_fn(target.priority * uncertainty)``, with the reward\n", + " shared among all simultaneous images of the same target.\n", + "\n", + " Args:\n", + " new_data_dict: Record of new images for each satellite\n", + "\n", + " Returns:\n", + " reward: Cumulative reward across satellites for one step\n", + " \"\"\"\n", + " reward = {}\n", + " imaged_counts = {}\n", + " for new_data in new_data_dict.values():\n", + " for target in new_data.imaged:\n", + " if target not in imaged_counts:\n", + " imaged_counts[target] = 0\n", + " imaged_counts[target] += 1\n", + "\n", + " for sat_id, new_data in new_data_dict.items():\n", + " reward[sat_id] = 0.0\n", + " for target in new_data.imaged:\n", + " uncertainty = target.search_area / ((100.0 * 1e3) ** 2)\n", + " reward[sat_id] += (\n", + " self.reward_fn(target.priority * uncertainty)\n", + " / imaged_counts[target]\n", + " )\n", + " return reward" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuring the Satellite to Have Access to Target Uncertainty Information\n", + "\n", + "The satellite has observations and actions associated with it that are relevant to the decision-making process. The observation space can be modified to include information about the targets and the uncertainty which allows better informed decision-making.\n", + "\n", + "* [Observations](../api_reference/obs/index.rst): \n", + " - SatProperties: Body angular velocity, instrument pointing direction, body position, and body velocity in [dynamics model](../api_reference/sim/dyn/index.rst).\n", + " - OpportunityProperties: Target priority, pointing error, pointing rate error, time until the target window opens, time until the target window closes, and uncertainty (normalized search area) for the next 32 targets.\n", + " - Density: Upcoming target request density.\n", + "* [Actions](../api_reference/act/index.rst):\n", + " - Image: Image target from upcoming 32 targets\n", + "* [Dynamics model](../api_reference/sim/dyn/index.rst): FullFeaturedDynModel is used.\n", + "* [Flight software model](../api_reference/sim/fsw/index.rst): SteeringImagerFSWModel used as the base model and modified for duration-based imaging." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if TYPE_CHECKING: # pragma: no cover\n", + " from bsk_rl.scene.targets import (\n", + " Target,\n", + " )\n", + "\n", + "\n", + "class Density(obs.Observation):\n", + " def __init__(\n", + " self,\n", + " interval_duration=60 * 3,\n", + " intervals=10,\n", + " norm=3,\n", + " ):\n", + " self.satellite: \"sats.ImagingSatellite\"\n", + " super().__init__()\n", + " self.interval_duration = interval_duration\n", + " self.intervals = intervals\n", + " self.norm = norm\n", + "\n", + " def get_obs(self):\n", + " if self.intervals == 0:\n", + " return []\n", + "\n", + " self.satellite.calculate_additional_windows(\n", + " self.simulator.sim_time\n", + " + (self.intervals + 1) * self.interval_duration\n", + " - self.satellite.window_calculation_time\n", + " )\n", + " soonest = self.satellite.upcoming_opportunities_dict(types=\"target\")\n", + " rewards = np.array([opportunity.priority for opportunity in soonest])\n", + " times = np.array([opportunities[0][1] for opportunities in soonest.values()])\n", + " time_bins = np.floor((times - self.simulator.sim_time) / self.interval_duration)\n", + " densities = [sum(rewards[time_bins == i]) for i in range(self.intervals)]\n", + " return np.array(densities) / self.norm\n", + "\n", + "\n", + "class CustomFswModel(fsw.SteeringImagerFSWModel):\n", + " @action\n", + " def action_image(\n", + " self,\n", + " r_LP_P: Iterable[float],\n", + " data_name: str,\n", + " acquisitionTime: float,\n", + " allowedTime: float,\n", + " ) -> None:\n", + " \"\"\"Attempt to image a target at a location.\n", + "\n", + " This action sets the target attitude to one tracking a ground location. If the\n", + " target is within the imaging constraints for the specified time, an image will be taken and stored in\n", + " the data buffer. The instrument power sink will be active as long as the task is\n", + " enabled.\n", + "\n", + " Args:\n", + " r_LP_P: [m] Planet-fixed planet relative target location.\n", + " data_name: Data buffer to store image data to.\n", + " acquisitionTime: [s] Time required to acquire the image.\n", + " allowedTime: [s] Maximum time allowed to attempt to acquire the image.\n", + " \"\"\"\n", + " self.insControl.controllerStatus = 1\n", + " self.dynamics.instrumentPowerSink.powerStatus = 1\n", + " self.dynamics.imagingTarget.r_LP_P_Init = r_LP_P\n", + " self.dynamics.instrument.nodeDataName = data_name\n", + " self.insControl.imaged = 0\n", + " self.insControl.useDurationImaging = True\n", + " self.insControl.acquisitionTime = macros.sec2nano(acquisitionTime)\n", + " self.insControl.allowedTime = macros.sec2nano(allowedTime)\n", + " self.simulator.enableTask(self.LocPointTask.name + self.satellite.name)\n", + "\n", + "\n", + "class CustomSatComposed(sats.ImagingSatellite):\n", + " observation_spec = [\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=orbitalMotion.REQ_EARTH * 1e3),\n", + " dict(prop=\"v_BN_P\", norm=7616.5),\n", + " ),\n", + " Density(intervals=20, norm=5),\n", + " obs.OpportunityProperties(\n", + " dict(prop=\"priority\"),\n", + " dict(prop=\"r_LB_H\", norm=orbitalMotion.REQ_EARTH * 1e3),\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.0),\n", + " dict(prop=\"opportunity_close\", norm=300.0),\n", + " dict(\n", + " prop=\"search_area\",\n", + " fn=lambda sat, opp: opp[\"object\"].search_area,\n", + " norm=(100.0 * 1e3) ** 2,\n", + " ),\n", + " type=\"target\",\n", + " n_ahead_observe=32,\n", + " ),\n", + " ]\n", + "\n", + " action_spec = [\n", + " act.Image(n_ahead_image=32),\n", + " ]\n", + "\n", + " dyn_type = dyn.FullFeaturedDynModel\n", + " fsw_type = CustomFswModel\n", + "\n", + " def task_target_for_imaging(\n", + " self, target: \"Target\", max_duration: Optional[float] = None\n", + " ):\n", + " \"\"\"Task the satellite to image a target.\n", + "\n", + " Args:\n", + " target: Selected target\n", + " max_duration: [s] Maximum duration to wait for imaging. If None, wait until\n", + " the end of the target's access window.\n", + " \"\"\"\n", + " msg = f\"{target} tasked for imaging, image_time: {target.image_time:.1f}, duration: {target.search_duration:.1f}\"\n", + " self.logger.info(msg)\n", + " self.fsw.action_image(\n", + " target.r_LP_P, self.buffer_name, target.image_time, target.search_duration\n", + " ) # times in seconds\n", + " self.enable_target_window(target, max_duration=max_duration)\n", + " self.draw_imaging_line(target)\n", + " self.latest_target = target" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When instantiating a satellite, these parameters can be overriden with a constant or \n", + "rerandomized every time the environment is reset using the ``sat_args`` dictionary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataStorageCapacity = (\n", + " 20 * 8e6 * 100\n", + ") # Large storage to avoid filling up in three orbits\n", + "batteryStorageCapacity = (\n", + " 1000.0 * 3600 * 2\n", + ") # Large storage to avoid battery running out in three orbits\n", + "ALTITUDE = 800\n", + "T_ORBIT = ( # Orbital period\n", + " 2\n", + " * np.pi\n", + " * np.sqrt((orbitalMotion.REQ_EARTH + ALTITUDE) ** 3 / orbitalMotion.MU_EARTH)\n", + ")\n", + "sat_args = CustomSatComposed.default_sat_args(\n", + " oe=lambda: random_orbit(\n", + " alt=ALTITUDE, # 800 km altitude\n", + " i=45, # 45 degrees inclination\n", + " ),\n", + " imageAttErrorRequirement=0.01,\n", + " imageRateErrorRequirement=0.01,\n", + " batteryStorageCapacity=batteryStorageCapacity,\n", + " storedCharge_Init=lambda: np.random.uniform(0.4, 1.0) * batteryStorageCapacity,\n", + " u_max=0.4,\n", + " K1=0.25,\n", + " K3=3.0,\n", + " servo_P=150 / 5,\n", + " nHat_B=np.array([0, 0, -1]),\n", + " omega_max=np.radians(5.0), # Maximum rate command in degrees per second\n", + " imageTargetMinimumElevation=np.arctan(800 / 500), # 58 degrees minimum elevation\n", + " rwBasePower=20,\n", + " maxWheelSpeed=1500,\n", + " storageInit=lambda: np.random.randint(\n", + " 0 * dataStorageCapacity,\n", + " 0.01 * dataStorageCapacity,\n", + " ), # Initialize storage use close to zero\n", + " wheelSpeeds=lambda: np.random.uniform(\n", + " -1, 1, 3\n", + " ), # Initialize reaction wheel speeds close to zero\n", + " dataStorageCapacity=dataStorageCapacity,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Making Target Uncertainty Growing Environment\n", + "\n", + "In order to model the growth of target location uncertainty over time, the step function of the gym environment is modified with the [SatelliteTasking](../api_reference/index.rst) base class. The target uncertainty grows based on each time step and the target's speed which is set in the `AreaTargets` class. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class SatelliteUncertaintyTargets(SatelliteTasking):\n", + " def step(self, action) -> tuple[Any, float, bool, bool, dict[str, Any]]:\n", + " \"\"\"Take a single-satellite action, run parent step, and update target uncertainties.\"\"\"\n", + " target_selected = self.satellites[0].parse_target_selection(action)\n", + " obs, reward, terminated, truncated, info = super().step(action)\n", + " d_ts = info[\"d_ts\"]\n", + "\n", + " targets = self.satellites[0].data_store.data.known\n", + " area_factor = 20e3 # scan width in meters (20 km)\n", + "\n", + " # Compute all new values in vectorized form\n", + " search_areas = np.array([t.search_area for t in targets])\n", + " side_lengths = np.sqrt(search_areas)\n", + " acquiring_speeds = np.array([t.acquiring_speed for t in targets])\n", + " speeds = np.array([t.speed for t in targets])\n", + "\n", + " new_side_lengths = side_lengths + speeds * d_ts\n", + " new_search_areas = new_side_lengths**2\n", + " new_search_durations = (\n", + " new_side_lengths\n", + " * np.ceil(new_side_lengths / area_factor)\n", + " / acquiring_speeds\n", + " )\n", + "\n", + " means = new_search_durations / 2.0\n", + " stds = new_search_durations * 0.3\n", + " new_image_times = np.maximum(0.0, np.random.normal(loc=means, scale=stds))\n", + "\n", + " # Update the target information\n", + " for t, area, dur, time in zip(\n", + " targets, new_search_areas, new_search_durations, new_image_times\n", + " ):\n", + " t.search_area = area\n", + " t.search_duration = dur\n", + " t.image_time = time\n", + "\n", + " # Reset the imaged target's uncertainty\n", + " if reward > 0.0:\n", + " target_selected.search_area = 0.0\n", + " target_selected.search_duration = 0.0\n", + " target_selected.image_time = 0.0\n", + "\n", + " return obs, reward, terminated, truncated, info" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initializing and Interacting with the Environment\n", + "For this example, the custom environment created above is used along with passing the satellite that is configured, the environment takes a [scenario](../api_reference/scene/index.rst), which defines the environment the satellite is acting in, and a [rewarder](../api_reference/data/index.rst), which defines how data collected from the scenario is rewarded." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "satellite = CustomSatComposed(\"EO\", sat_args)\n", + "n_targets = (1000, 10000)\n", + "\n", + "env = SatelliteUncertaintyTargets(\n", + " satellite=satellite,\n", + " world_type=world.GroundStationWorldModel,\n", + " world_args=world.GroundStationWorldModel.default_world_args(),\n", + " scenario=AreaTargets(n_targets=n_targets),\n", + " rewarder=UncertainMultiplyReward(),\n", + " sim_rate=0.5,\n", + " max_step_duration=T_ORBIT, # Duration long enough to prevent interruption of the imaging action\n", + " time_limit=T_ORBIT * 3, # three orbits\n", + " log_level=\"INFO\",\n", + " failure_penalty=0.0,\n", + " # disable_env_checker=True, # For debugging\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, reset the environment. It is possible to specify the seed when resetting the environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "observation, info = env.reset(seed=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is possible to print out the actions and observations. The composed satellite [action_description](../api_reference/sats/index.rst) returns a human-readable action map. Each satellite has the same action space and similar observation space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Actions:\", env.satellites[0].action_description, \"\\n\")\n", + "print(\"States:\", env.unwrapped.satellites[0].observation_description, \"\\n\")\n", + "\n", + "# Using the composed satellite features also provides a human-readable state:\n", + "for satellite in env.unwrapped.satellites:\n", + " for k, v in satellite.observation_builder.obs_dict().items():\n", + " print(f\"{k}: {v}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, run the simulation until timeout." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "while True:\n", + " action_step = np.random.randint(0, 32) # Random action\n", + "\n", + " observation, reward, terminated, truncated, info = env.step(action_step)\n", + "\n", + " if terminated or truncated:\n", + " print(\"Episode complete.\")\n", + " break" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After running the simulation, we can check the total reward, the number of unique targets that were imaged at least once, and the number of duplicated (re-imaged) targets collected by the rewarder." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Total reward:\", env.unwrapped.rewarder.cum_reward)\n", + "print(\n", + " \"Number of unique imaged targets:\",\n", + " len(set(env.unwrapped.rewarder.data.imaged)),\n", + ")\n", + "print(\n", + " \"Number of duplicated targets:\",\n", + " env.unwrapped.rewarder.data.duplicates,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Check [Training with RLlib PPO](../examples/rllib_training.ipynb) for an example on how to train the agent in this environment." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 1de6fe1d52b593e94549ac66bc8ff395f473038e Mon Sep 17 00:00:00 2001 From: Yumeka Nagano <> Date: Thu, 18 Dec 2025 11:24:59 -0700 Subject: [PATCH 2/2] [#325] Edit documetation for target uncertainty example --- docs/source/release_notes.rst | 1 + examples/_default.rst | 1 + pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst index c5714c64..5ef3e741 100644 --- a/docs/source/release_notes.rst +++ b/docs/source/release_notes.rst @@ -14,6 +14,7 @@ Development - |version| * Allow for the Vizard output path to be specified as a .bin file instead of just a directory. * Use Vizard 2.3.1 locations for visualization; results in significantly smaller output files. +* Add an example script demonstrating target location uncertainty modeling with uncertainty growth over time: `Target Location Uncertainty `_ example. * Allow for a simpler Earth model to be used in Vizard by setting ``use_simple_earth=True`` in the Vizard settings dictionary. This is helpful for when visualizing may Earth-fixed targets. diff --git a/examples/_default.rst b/examples/_default.rst index a3d10427..c0666bcf 100644 --- a/examples/_default.rst +++ b/examples/_default.rst @@ -21,6 +21,7 @@ Earth Observation cloud_environment cloud_environment_with_reimaging aeos + target_location_uncertainty RSO Inspection ~~~~~~~~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index bba230cb..b3158b05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ readme = "README.md" requires-python = ">=3.10.0" license = { text = "MIT" } dependencies = [ - "bsk>=2.8.9", + "bsk @ git+https://github.com/AVSLab/basilisk.git@develop", "Deprecated", "gymnasium", "numpy",