From 175bb4c5b468f4dda1f1f70b070f89e234a22048 Mon Sep 17 00:00:00 2001 From: Tucker Date: Sat, 6 Jan 2024 00:16:27 -0500 Subject: [PATCH 1/6] Fix `observe_from()`. --- CHANGELOG.md | 7 + smarts/core/plan.py | 7 +- smarts/core/sensor.py | 6 +- smarts/core/sensor_manager.py | 34 ++-- smarts/core/sensors/__init__.py | 15 +- smarts/core/smarts.py | 63 +++++++- smarts/core/vehicle.py | 270 +++++++++++++++++++------------- smarts/core/vehicle_index.py | 6 +- 8 files changed, 271 insertions(+), 137 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9467aa210..6e481b9f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,16 @@ Copy and pasting the git commit messages is __NOT__ enough. ## [Unreleased] - XXXX-XX-XX ### Added +- Added a utility method `SMARTS.prepare_observe_from()` which allows safely adding sensors to vehicles. +- The following methods now exist explicitly `Vehicle.{add_sensor|detach_sensor|subscribed_to|sensor_property|}`. ### Changed - `VehicleIndex.build_agent_vehicle()` no longer has `filename` and `surface_patches` parameters. - The following modules have been renamed: `envision.types` -> `envision.etypes`, `smarts.core.utils.logging` -> `smarts.core.utils.core_logging`, `smarts.core.utils.math` -> `smarts.core.utils.core_math`, `smarts.sstudio.types` -> `smarts.sstudio.sstypes`. For compatibility reasons they can still be imported by their original module name. - Exposed `traffic:traci_retries`/`SMARTS_TRAFFIC_TRACI_RETRIES` to control how many times the `SumoTrafficSimulation` will try to restart when using default configuration. - `rllib` is now constrained as `<=2.9,>2.4`. - The `examples/e12_rllib` training examples `{pg_example|pg_pbt_example}.py` have been changed to `{ppo_example|ppo_pbt_example}.py`. `Policy Gradients (PG)` has been dropped in favor of the more well documented `Proximal Policy Optimization (PPO)`. +- Vehicles can now have sensors added to, overwritten, or replaced outright. +- Logging is now improved to give information about sensor changes in the sensor manager. ### Deprecated ### Fixed - `SumoTrafficSimulation` gives clearer reasons as to why it failed to connect to the TraCI server. @@ -34,6 +38,9 @@ Copy and pasting the git commit messages is __NOT__ enough. - Fixed an issue where you would need to install `waymo` in order to use any kind of dataset histories. - Fixed an issue where Pycharm would load `smarts/sstudio/types` as the integrated `types` module. See #2125. - Fixed an issue where the `e12_rllib` examples would use the wrong path for the default loop scenario. +- Fixed an issue where the sensor state could be `None` when calling `SMARTS.observe_from()` on a non-ego vehicle. See #2133. +- The via sensor and trip meter sensor now work without a mission. +- Fixed a bug with `VehicleIndex.attach_sensors_to_vehicle()` that would generate an invalid plan. ### Removed ### Security diff --git a/smarts/core/plan.py b/smarts/core/plan.py index 7ceef9a8eb..a9ac288df0 100644 --- a/smarts/core/plan.py +++ b/smarts/core/plan.py @@ -26,7 +26,7 @@ import sys import warnings from dataclasses import dataclass, field -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Type import numpy as np @@ -301,6 +301,11 @@ class PlanFrame: road_ids: List[str] mission: Optional[Mission] + @classmethod + def empty(cls: Type[PlanFrame]): + """Creates an empty plan frame.""" + return cls(road_ids=[], mission=None) + class Plan: """Describes a navigation plan (route) to fulfill a mission.""" diff --git a/smarts/core/sensor.py b/smarts/core/sensor.py index 0c38e60eff..d4d72fa283 100644 --- a/smarts/core/sensor.py +++ b/smarts/core/sensor.py @@ -394,8 +394,9 @@ def update_distance_wps_record( wp_road = road_map.lane_by_id(new_wp.lane_id).road.road_id should_count_wp = ( + plan.mission == None # if we do not have a fixed route, we count all waypoints we accumulate - not plan.mission.requires_route + or not plan.mission.requires_route # if we have a route to follow, only count wps on route or wp_road in [road.road_id for road in plan.route.roads] ) @@ -664,6 +665,9 @@ def __eq__(self, __value: object) -> bool: def __call__(self, vehicle_state: VehicleState, plan, road_map): near_points: List[ViaPoint] = list() hit_points: List[ViaPoint] = list() + if plan.mission is None: + return (near_points, hit_points) + vehicle_position = vehicle_state.pose.position[:2] @lru_cache() diff --git a/smarts/core/sensor_manager.py b/smarts/core/sensor_manager.py index b3a85523b9..7a325ef1d5 100644 --- a/smarts/core/sensor_manager.py +++ b/smarts/core/sensor_manager.py @@ -38,13 +38,12 @@ from smarts.core.simulation_frame import SimulationFrame from smarts.core.simulation_local_constants import SimulationLocalConstants -logger = logging.getLogger(__name__) - class SensorManager: """A sensor management system that associates actors with sensors.""" def __init__(self): + self._logger = logging.getLogger(self.__class__.__name__) self._sensors: Dict[str, Sensor] = {} # {actor_id: } @@ -55,7 +54,7 @@ def __init__(self): self._actors_by_sensor_id: Dict[str, Set[str]] = {} self._sensor_references = Counter() # {sensor_id, ...} - self._scheduled_sensors: List[Sensor] = [] + self._scheduled_sensors: List[Tuple[str, Sensor]] = [] observation_workers = config()( "core", "observation_workers", default=0, cast=int ) @@ -168,6 +167,7 @@ def observe_batch( def teardown(self, renderer): """Tear down the current sensors and clean up any internal resources.""" + self._logger.info("++ Sensors and sensor states reset. ++") for sensor in self._sensors.values(): sensor.teardown(renderer=renderer) self._sensors = {} @@ -178,23 +178,31 @@ def teardown(self, renderer): def add_sensor_state(self, actor_id: str, sensor_state: SensorState): """Add a sensor state associated with a given actor.""" + self._logger.info("Sensor state added for actor '%s'.", actor_id) self._sensor_states[actor_id] = sensor_state - def remove_sensors_by_actor_id( + def remove_sensor_state_by_actor_id(self, actor_id: str): + """Add a sensor state associated with a given actor.""" + self._logger.info("Sensor state removed for actor '%s'.", actor_id) + del self._sensor_states[actor_id] + + def remove_actor_sensors_by_actor_id( self, actor_id: str, schedule_teardown: bool = True ) -> Iterable[Tuple[Sensor, int]]: """Remove association of an actor to sensors. If the sensor is no longer associated an actor, the sensor is scheduled to be removed.""" sensor_states = self._sensor_states.get(actor_id) if not sensor_states: - logger.warning( - "Attempted to remove sensors from actor with no sensors: `%s`", actor_id + self._logger.warning( + "Attempted to remove sensors from actor with no sensors: '%s'.", + actor_id, ) return [] - del self._sensor_states[actor_id] + self.remove_sensor_state_by_actor_id(actor_id) sensors_by_actor = self._sensors_by_actor_id.get(actor_id) if not sensors_by_actor: return [] + self._logger.info("Target sensor removal for actor '%s'.", actor_id) discarded_sensors = [] for sensor_id in sensors_by_actor: self._actors_by_sensor_id[sensor_id].remove(actor_id) @@ -210,6 +218,7 @@ def remove_sensor( self, sensor_id: str, schedule_teardown: bool = False ) -> Optional[Sensor]: """Remove a sensor by its id. Removes any associations it has with actors.""" + self._logger.info("Target removal of sensor '%s'.", sensor_id) sensor = self._sensors.get(sensor_id) if not sensor: return None @@ -218,8 +227,9 @@ def remove_sensor( def _disassociate_sensor(self, sensor_id, schedule_teardown): if schedule_teardown: - self._scheduled_sensors.append(self._sensors[sensor_id]) + self._scheduled_sensors.append((sensor_id, self._sensors[sensor_id])) + self._logger.info("Sensor '%s' removed from manager.", sensor_id) del self._sensors[sensor_id] del self._sensor_references[sensor_id] @@ -275,7 +285,7 @@ def add_sensor_for_actor(self, actor_id: str, name: str, sensor: Sensor) -> str: s_id = SensorManager._actor_and_sensor_name_to_sensor_id(name, actor_id) actor_sensors = self._sensors_by_actor_id.setdefault(actor_id, set()) if s_id in actor_sensors: - logger.warning( + self._logger.warning( "Duplicate sensor attempted to add to actor `%s`: `%s`", actor_id, s_id ) return s_id @@ -286,6 +296,7 @@ def add_sensor_for_actor(self, actor_id: str, name: str, sensor: Sensor) -> str: def add_sensor(self, sensor_id, sensor: Sensor) -> str: """Adds a sensor to the sensor manager.""" + self._logger.info("Added sensor '%s' to sensor manager.", sensor_id) assert sensor_id not in self._sensors self._sensors[sensor_id] = sensor self._sensor_references.update([sensor_id]) @@ -298,9 +309,10 @@ def clean_up_sensors_for_actors(self, current_actor_ids: Set[str], renderer): missing_actors = old_actor_ids - current_actor_ids for aid in missing_actors: - self.remove_sensors_by_actor_id(aid) + self.remove_actor_sensors_by_actor_id(aid) - for sensor in self._scheduled_sensors: + for sensor_id, sensor in self._scheduled_sensors: + self._logger.info("Sensor '%s' destroyed.", sensor_id) sensor.teardown(renderer=renderer) self._scheduled_sensors.clear() diff --git a/smarts/core/sensors/__init__.py b/smarts/core/sensors/__init__.py index e4b4ca2a7a..2509425a7b 100644 --- a/smarts/core/sensors/__init__.py +++ b/smarts/core/sensors/__init__.py @@ -17,11 +17,13 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import annotations + import logging import math import re import sys -from typing import Any, Dict, List, Optional, Sequence, Set, Tuple +from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type import numpy as np @@ -111,7 +113,7 @@ def _make_vehicle_observation( class SensorState: """Sensor state information""" - def __init__(self, max_episode_steps: int, plan_frame: PlanFrame): + def __init__(self, max_episode_steps: Optional[int], plan_frame: PlanFrame): self._max_episode_steps = max_episode_steps self._plan_frame = plan_frame self._step = 0 @@ -157,6 +159,11 @@ def steps_completed(self) -> int: """Get the number of steps where this sensor has been updated.""" return self._step + @classmethod + def invalid(cls: Type[SensorState]) -> SensorState: + """Generate a invalid default frame.""" + return cls(None, PlanFrame.empty()) + class SensorResolver: """An interface describing sensor observation and update systems.""" @@ -272,7 +279,7 @@ def process_serialization_safe_sensors( sim_frame: SimulationFrame, sim_local_constants: SimulationLocalConstants, interface: AgentInterface, - sensor_state, + sensor_state: SensorState, vehicle_id, agent_id=None, ): @@ -652,7 +659,7 @@ def _is_done_with_events( def _agent_reached_goal( cls, sensor_state, plan, vehicle_state: VehicleState, trip_meter_sensor ): - if not trip_meter_sensor: + if not trip_meter_sensor or plan.mission is None: return False distance_travelled = trip_meter_sensor() mission = plan.mission diff --git a/smarts/core/smarts.py b/smarts/core/smarts.py index a6eaffbbc5..05fc4baf99 100644 --- a/smarts/core/smarts.py +++ b/smarts/core/smarts.py @@ -45,6 +45,7 @@ from smarts.core.id_actor_capture_manager import IdActorCaptureManager from smarts.core.plan import Plan from smarts.core.renderer_base import RendererBase +from smarts.core.sensors import SensorState from smarts.core.simulation_local_constants import SimulationLocalConstants from smarts.core.utils.core_logging import timeit from smarts.core.utils.type_operations import TypeSuite @@ -755,7 +756,7 @@ def vehicle_exited_bubble( if teardown_agent: self.teardown_social_agents([shadow_agent_id]) if self._vehicle_index.shadower_id_from_vehicle_id(vehicle_id) is None: - self._sensor_manager.remove_sensors_by_actor_id(vehicle_id) + self._sensor_manager.remove_actor_sensors_by_actor_id(vehicle_id) def _agent_releases_actor( self, @@ -951,28 +952,76 @@ def _teardown_vehicles(self, vehicle_ids): for v_id in vehicle_ids: self._remove_vehicle_from_providers(v_id) - def attach_sensors_to_vehicles(self, agent_interface, vehicle_ids): + def attach_sensors_to_vehicles( + self, + agent_interface: AgentInterface, + vehicle_ids, + overwrite_sensors=False, + reset_sensors=False, + ): """Set the specified vehicles with the sensors needed to satisfy the specified agent - interface. + interface. See :attr:`smarts.core.smarts.SMARTS.prepare_observe_from`. + + Args: + agent_interface (AgentInterface): The minimum interface generating the observations. + vehicle_ids (Sequence[str]): The vehicles to target. + overwrite_sensors (bool, optional): If to replace existing sensors (USE CAREFULLY). Defaults to False. + reset_sensors (bool, optional): If to remove all existing sensors before adding sensors (USE **VERY** CAREFULLY). Defaults to False. + """ + self.prepare_observe_from( + vehicle_ids, agent_interface, overwrite_sensors, reset_sensors + ) + + def prepare_observe_from( + self, + vehicle_ids: Sequence[str], + interface: AgentInterface, + overwrite_sensors, + reset_sensors, + ): + """Assigns the given vehicle sensors as is described by the agent interface. + + Args: + vehicle_ids (Sequence[str]): The vehicles to target. + interface (AgentInterface): The minimum interface generating the observations. + overwrite_sensors (bool, optional): If to replace existing sensors (USE CAREFULLY). + reset_sensors (bool, optional): If to remove all existing sensors (USE **VERY** CAREFULLY). """ self._check_valid() + for v_id in vehicle_ids: v = self._vehicle_index.vehicle_by_id(v_id) Vehicle.attach_sensors_to_vehicle( - self._sensor_manager, self, v, agent_interface + self.sensor_manager, + self, + v, + interface, + replace=overwrite_sensors, + reset_sensors=reset_sensors, ) def observe_from( - self, vehicle_ids: Sequence[str], interface: AgentInterface + self, + vehicle_ids: Sequence[str], + interface: AgentInterface, ) -> Tuple[Dict[str, Observation], Dict[str, bool]]: - """Generate observations from the specified vehicles.""" + """Generate observations from the specified vehicles. + + Args: + vehicle_ids (Sequence[str]): The vehicles to target. + interface (AgentInterface): The intended interface generating the observations (this may be ignored.) + + Returns: + Tuple[Dict[str, Observation], Dict[str, bool]]: A dictionary of observations and the hypothetical dones. + """ self._check_valid() - vehicles = { + vehicles: Dict[str, Vehicle] = { v_id: self.vehicle_index.vehicle_by_id(v_id) for v_id in vehicle_ids } sensor_states = { vehicle.id: self._sensor_manager.sensor_state_for_actor_id(vehicle.id) + or SensorState.invalid() for vehicle in vehicles.values() } return self.sensor_manager.observe_batch( diff --git a/smarts/core/vehicle.py b/smarts/core/vehicle.py index 9aca717f4b..bf9c31006b 100644 --- a/smarts/core/vehicle.py +++ b/smarts/core/vehicle.py @@ -21,8 +21,8 @@ import logging import os from dataclasses import dataclass -from functools import lru_cache -from typing import Any, Dict, List, Optional, Tuple, Union +from functools import lru_cache, partial +from typing import Any, Dict, List, Optional, Tuple, Type, Union import numpy as np @@ -59,6 +59,21 @@ class Vehicle: """Represents a single vehicle.""" _HAS_DYNAMIC_ATTRIBUTES = True # dynamic pytype attribute + _sensor_names = [ + "ogm_sensor", + "rgb_sensor", + "lidar_sensor", + "driven_path_sensor", + "trip_meter_sensor", + "drivable_area_grid_map_sensor", + "neighborhood_vehicle_states_sensor", + "waypoints_sensor", + "road_waypoints_sensor", + "accelerometer_sensor", + "lane_position_sensor", + "via_sensor", + "signals_sensor", + ] def __init__( self, @@ -404,104 +419,135 @@ def build_social_vehicle(sim, vehicle_id, vehicle_state: VehicleState) -> "Vehic vehicle_config_type=vehicle_state.vehicle_config_type, ) - @staticmethod + @classmethod def attach_sensors_to_vehicle( + cls, sensor_manager, sim, vehicle: "Vehicle", agent_interface, + replace=True, + reset_sensors=False, ): """Attach sensors as required to satisfy the agent interface's requirements""" # The distance travelled sensor is not optional b/c it is used for the score # and reward calculation vehicle_state = vehicle.state - sensor = TripMeterSensor() - vehicle.attach_trip_meter_sensor(sensor) - - # The distance travelled sensor is not optional b/c it is used for visualization - # done criteria - sensor = DrivenPathSensor() - vehicle.attach_driven_path_sensor(sensor) - - if agent_interface.neighborhood_vehicle_states: - sensor = NeighborhoodVehiclesSensor( - radius=agent_interface.neighborhood_vehicle_states.radius, - ) - vehicle.attach_neighborhood_vehicle_states_sensor(sensor) - - if agent_interface.accelerometer: - sensor = AccelerometerSensor() - vehicle.attach_accelerometer_sensor(sensor) - - if agent_interface.lane_positions: - sensor = LanePositionSensor() - vehicle.attach_lane_position_sensor(sensor) + has_no_sensors = len(vehicle.sensors) == 0 + added_sensors: List[Tuple[str, Sensor]] = [] - if agent_interface.waypoint_paths: - sensor = WaypointsSensor( - lookahead=agent_interface.waypoint_paths.lookahead, - ) - vehicle.attach_waypoints_sensor(sensor) + if reset_sensors: + sensor_manager.remove_actor_sensors_by_actor_id(vehicle.id) + # pytype: disable=attribute-error + Vehicle.detach_all_sensors_from_vehicle(vehicle) + # pytype: enable=attribute-error - if agent_interface.road_waypoints: - sensor = RoadWaypointsSensor( - horizon=agent_interface.road_waypoints.horizon, - ) - vehicle.attach_road_waypoints_sensor(sensor) + def add_sensor_if_needed( + sensor_type: Type[Sensor], + sensor_name: str, + condition: bool = True, + **kwargs, + ): + assert sensor_name in cls._sensor_names + if ( + replace + or has_no_sensors + or (condition and not vehicle.subscribed_to(sensor_name)) + ): + sensor = sensor_type(**kwargs) + vehicle.attach_sensor(sensor, sensor_name) + added_sensors.append((sensor_name, sensor)) + + add_sensor_if_needed(TripMeterSensor, sensor_name="trip_meter_sensor") + add_sensor_if_needed(DrivenPathSensor, sensor_name="driven_path_sensor") + add_sensor_if_needed( + NeighborhoodVehiclesSensor, + sensor_name="neighborhood_vehicle_states_sensor", + condition=agent_interface.neighborhood_vehicle_states, + radius=agent_interface.neighborhood_vehicle_states.radius, + ) + add_sensor_if_needed( + AccelerometerSensor, + sensor_name="accelerometer_sensor", + condition=agent_interface.accelerometer, + ) + add_sensor_if_needed( + WaypointsSensor, + sensor_name="waypoints_sensor", + condition=agent_interface.waypoint_paths, + ) + add_sensor_if_needed( + RoadWaypointsSensor, + "road_waypoints_sensor", + condition=agent_interface.road_waypoints, + horizon=agent_interface.road_waypoints.horizon, + ) + add_sensor_if_needed( + LanePositionSensor, + "lane_position_sensor", + condition=agent_interface.lane_positions, + ) + # DrivableAreaGridMapSensor if agent_interface.drivable_area_grid_map: if not sim.renderer: raise RendererException.required_to("add a drivable_area_grid_map") - sensor = DrivableAreaGridMapSensor( + add_sensor_if_needed( + DrivableAreaGridMapSensor, + "drivable_area_grid_map_sensor", + True, # Always add this sensor vehicle_state=vehicle_state, width=agent_interface.drivable_area_grid_map.width, height=agent_interface.drivable_area_grid_map.height, resolution=agent_interface.drivable_area_grid_map.resolution, renderer=sim.renderer, ) - vehicle.attach_drivable_area_grid_map_sensor(sensor) + # OGMSensor if agent_interface.occupancy_grid_map: if not sim.renderer: raise RendererException.required_to("add an OGM") - sensor = OGMSensor( + add_sensor_if_needed( + OGMSensor, + "ogm_sensor", + True, # Always add this sensor vehicle_state=vehicle_state, width=agent_interface.occupancy_grid_map.width, height=agent_interface.occupancy_grid_map.height, resolution=agent_interface.occupancy_grid_map.resolution, renderer=sim.renderer, ) - vehicle.attach_ogm_sensor(sensor) + # RGBSensor if agent_interface.top_down_rgb: if not sim.renderer: raise RendererException.required_to("add an RGB camera") - sensor = RGBSensor( + add_sensor_if_needed( + RGBSensor, + "rgb_sensor", + True, # Always add this sensor vehicle_state=vehicle_state, width=agent_interface.top_down_rgb.width, height=agent_interface.top_down_rgb.height, resolution=agent_interface.top_down_rgb.resolution, renderer=sim.renderer, ) - vehicle.attach_rgb_sensor(sensor) - if agent_interface.lidar_point_cloud: - sensor = LidarSensor( - vehicle_state=vehicle_state, - sensor_params=agent_interface.lidar_point_cloud.sensor_params, - ) - vehicle.attach_lidar_sensor(sensor) - - sensor = ViaSensor( - # At lane change time of 6s and speed of 13.89m/s, acquistion range = 6s x 13.89m/s = 83.34m. - lane_acquisition_range=80, - speed_accuracy=1.5, + add_sensor_if_needed( + LidarSensor, + "lidar_sensor", + condition=agent_interface.lidar_point_cloud, + vehicle_state=vehicle_state, + sensor_params=agent_interface.lidar_point_cloud.sensor_params, + ) + add_sensor_if_needed( + ViaSensor, "via_sensor", True, lane_acquisition_range=80, speed_accuracy=1.5 + ) + add_sensor_if_needed( + SignalsSensor, + "signals_sensor", + condition=agent_interface.signals, + lookahead=agent_interface.signals.lookahead, ) - vehicle.attach_via_sensor(sensor) - - if agent_interface.signals: - lookahead = agent_interface.signals.lookahead - sensor = SignalsSensor(lookahead=lookahead) - vehicle.attach_signals_sensor(sensor) - for sensor_name, sensor in vehicle.sensors.items(): + for sensor_name, sensor in added_sensors: if not sensor: continue sensor_manager.add_sensor_for_actor(vehicle.id, sensor_name, sensor) @@ -581,62 +627,66 @@ def teardown(self, renderer, exclude_chassis=False): renderer.remove_vehicle_node(self._id) self._initialized = False + def attach_sensor(self, sensor, sensor_name): + """replace previously-attached sensor with this one + (to allow updating its parameters). + Sensors might have been attached to a non-agent vehicle + (for example, for observation collection from history vehicles), + but if that vehicle gets hijacked, we want to use the sensors + specified by the hijacking agent's interface.""" + self.detach_sensor(sensor_name) + self._log.debug("replacing existing %s on vehicle %s", sensor_name, self.id) + setattr(self, f"_{sensor_name}", sensor) + self._sensors[sensor_name] = sensor + + def detach_sensor(self, sensor_name): + """Detach a sensor by name.""" + sensor = getattr(self, f"_{sensor_name}", None) + if sensor is not None: + setattr(self, f"_{sensor_name}", None) + del self._sensors[sensor_name] + return sensor + + def subscribed_to(self, sensor_name): + """Confirm if the sensor is subscribed.""" + sensor = getattr(self, f"_{sensor_name}", None) + return sensor is not None + + def sensor_property(self, sensor_name): + """Call a sensor by name.""" + sensor = getattr(self, f"_{sensor_name}", None) + assert sensor is not None, f"{sensor_name} is not attached to {self.id}" + return sensor + def _meta_create_sensor_functions(self): # Bit of metaprogramming to make sensor creation more DRY - sensor_names = [ - "ogm_sensor", - "rgb_sensor", - "lidar_sensor", - "driven_path_sensor", - "trip_meter_sensor", - "drivable_area_grid_map_sensor", - "neighborhood_vehicle_states_sensor", - "waypoints_sensor", - "road_waypoints_sensor", - "accelerometer_sensor", - "lane_position_sensor", - "via_sensor", - "signals_sensor", - ] + sensor_names = self._sensor_names for sensor_name in sensor_names: - - def attach_sensor(self, sensor, sensor_name=sensor_name): - # replace previously-attached sensor with this one - # (to allow updating its parameters). - # Sensors might have been attached to a non-agent vehicle - # (for example, for observation collection from history vehicles), - # but if that vehicle gets hijacked, we want to use the sensors - # specified by the hijacking agent's interface. - detach = getattr(self, f"detach_{sensor_name}") - if detach: - detach(sensor_name) - self._log.debug( - f"replacing existing {sensor_name} on vehicle {self.id}" - ) - setattr(self, f"_{sensor_name}", sensor) - self._sensors[sensor_name] = sensor - - def detach_sensor(self, sensor_name=sensor_name): - sensor = getattr(self, f"_{sensor_name}", None) - if sensor is not None: - setattr(self, f"_{sensor_name}", None) - del self._sensors[sensor_name] - return sensor - - def subscribed_to(self, sensor_name=sensor_name): - sensor = getattr(self, f"_{sensor_name}", None) - return sensor is not None - - def sensor_property(self, sensor_name=sensor_name): - sensor = getattr(self, f"_{sensor_name}", None) - assert sensor is not None, f"{sensor_name} is not attached to {self.id}" - return sensor - setattr(Vehicle, f"_{sensor_name}", None) - setattr(Vehicle, f"attach_{sensor_name}", attach_sensor) - setattr(Vehicle, f"detach_{sensor_name}", detach_sensor) - setattr(Vehicle, f"subscribed_to_{sensor_name}", property(subscribed_to)) - setattr(Vehicle, f"{sensor_name}", property(sensor_property)) + setattr( + Vehicle, + f"attach_{sensor_name}", + partial(self.attach_sensor, sensor_name=sensor_name), + ) + setattr( + Vehicle, + f"detach_{sensor_name}", + partial(self.detach_sensor, sensor_name=sensor_name), + ) + setattr( + Vehicle, + f"subscribed_to_{sensor_name}", + property( + partial(self.__class__.subscribed_to, sensor_name=sensor_name) + ), + ) + setattr( + Vehicle, + f"{sensor_name}", + property( + partial(self.__class__.sensor_property, sensor_name=sensor_name) + ), + ) def detach_all_sensors_from_vehicle(vehicle): sensors = [] diff --git a/smarts/core/vehicle_index.py b/smarts/core/vehicle_index.py index 7e9324194a..eada93c587 100644 --- a/smarts/core/vehicle_index.py +++ b/smarts/core/vehicle_index.py @@ -445,7 +445,7 @@ def switch_control_to_agent( agent_interface: The agent interface for sensor requirements. """ - self._log.debug(f"Switching control of {agent_id} to {vehicle_id}") + self._log.debug("Switching control of '%s' to '%s'", vehicle_id, agent_id) vehicle_id, agent_id = _2id(vehicle_id), _2id(agent_id) if recreate: @@ -589,7 +589,7 @@ def attach_sensors_to_vehicle(self, sim, vehicle_id, agent_interface, plan): vehicle.id, SensorState( agent_interface.max_episode_steps, - plan_frame=plan.frame, + plan_frame=plan.frame(), ), ) self._controller_states[vehicle_id] = ControllerState.from_action_space( @@ -646,7 +646,7 @@ def _switch_control_to_agent_recreate( # Remove the old vehicle self.teardown_vehicles_by_vehicle_ids([vehicle.id], sim.renderer_ref) - sim.sensor_manager.remove_sensors_by_actor_id(vehicle.id) + sim.sensor_manager.remove_actor_sensors_by_actor_id(vehicle.id) # HACK: Directly remove the vehicle from the traffic provider (should do this via the sim instead) for traffic_sim in sim.traffic_sims: if traffic_sim.manages_actor(vehicle.id): From ac2c63f675b42428fe1deda5e11721ec09b9e125 Mon Sep 17 00:00:00 2001 From: Tucker Date: Sat, 6 Jan 2024 00:37:05 -0500 Subject: [PATCH 2/6] Fix sensor bug. --- smarts/core/vehicle.py | 54 +++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/smarts/core/vehicle.py b/smarts/core/vehicle.py index bf9c31006b..831745ced7 100644 --- a/smarts/core/vehicle.py +++ b/smarts/core/vehicle.py @@ -425,7 +425,7 @@ def attach_sensors_to_vehicle( sensor_manager, sim, vehicle: "Vehicle", - agent_interface, + agent_interface: AgentInterface, replace=True, reset_sensors=False, ): @@ -443,7 +443,7 @@ def attach_sensors_to_vehicle( # pytype: enable=attribute-error def add_sensor_if_needed( - sensor_type: Type[Sensor], + sensor_type, sensor_name: str, condition: bool = True, **kwargs, @@ -460,12 +460,12 @@ def add_sensor_if_needed( add_sensor_if_needed(TripMeterSensor, sensor_name="trip_meter_sensor") add_sensor_if_needed(DrivenPathSensor, sensor_name="driven_path_sensor") - add_sensor_if_needed( - NeighborhoodVehiclesSensor, - sensor_name="neighborhood_vehicle_states_sensor", - condition=agent_interface.neighborhood_vehicle_states, - radius=agent_interface.neighborhood_vehicle_states.radius, - ) + if agent_interface.neighborhood_vehicle_states: + add_sensor_if_needed( + NeighborhoodVehiclesSensor, + sensor_name="neighborhood_vehicle_states_sensor", + radius=agent_interface.neighborhood_vehicle_states.radius, + ) add_sensor_if_needed( AccelerometerSensor, @@ -477,12 +477,12 @@ def add_sensor_if_needed( sensor_name="waypoints_sensor", condition=agent_interface.waypoint_paths, ) - add_sensor_if_needed( - RoadWaypointsSensor, - "road_waypoints_sensor", - condition=agent_interface.road_waypoints, - horizon=agent_interface.road_waypoints.horizon, - ) + if agent_interface.road_waypoints: + add_sensor_if_needed( + RoadWaypointsSensor, + "road_waypoints_sensor", + horizon=agent_interface.road_waypoints.horizon, + ) add_sensor_if_needed( LanePositionSensor, "lane_position_sensor", @@ -530,22 +530,22 @@ def add_sensor_if_needed( resolution=agent_interface.top_down_rgb.resolution, renderer=sim.renderer, ) - add_sensor_if_needed( - LidarSensor, - "lidar_sensor", - condition=agent_interface.lidar_point_cloud, - vehicle_state=vehicle_state, - sensor_params=agent_interface.lidar_point_cloud.sensor_params, - ) + if agent_interface.lidar_point_cloud: + add_sensor_if_needed( + LidarSensor, + "lidar_sensor", + vehicle_state=vehicle_state, + sensor_params=agent_interface.lidar_point_cloud.sensor_params, + ) add_sensor_if_needed( ViaSensor, "via_sensor", True, lane_acquisition_range=80, speed_accuracy=1.5 ) - add_sensor_if_needed( - SignalsSensor, - "signals_sensor", - condition=agent_interface.signals, - lookahead=agent_interface.signals.lookahead, - ) + if agent_interface.signals: + add_sensor_if_needed( + SignalsSensor, + "signals_sensor", + lookahead=agent_interface.signals.lookahead, + ) for sensor_name, sensor in added_sensors: if not sensor: From 19ef237bd10f81e91222272f32f38b0ddbed993f Mon Sep 17 00:00:00 2001 From: Tucker Date: Sat, 6 Jan 2024 12:57:13 -0500 Subject: [PATCH 3/6] Fix bug where instance attach properties were actually attached to the class. --- smarts/core/sensor_manager.py | 3 +- smarts/core/tests/test_vehicle.py | 40 +++++++++++++++++- smarts/core/utils/class_factory.py | 10 ++--- smarts/core/vehicle.py | 65 +++++++++++++++++++++--------- smarts/core/vehicle_index.py | 2 + 5 files changed, 92 insertions(+), 28 deletions(-) diff --git a/smarts/core/sensor_manager.py b/smarts/core/sensor_manager.py index 7a325ef1d5..521528072f 100644 --- a/smarts/core/sensor_manager.py +++ b/smarts/core/sensor_manager.py @@ -44,6 +44,7 @@ class SensorManager: def __init__(self): self._logger = logging.getLogger(self.__class__.__name__) + self._logger.setLevel(logging.INFO) self._sensors: Dict[str, Sensor] = {} # {actor_id: } @@ -184,7 +185,7 @@ def add_sensor_state(self, actor_id: str, sensor_state: SensorState): def remove_sensor_state_by_actor_id(self, actor_id: str): """Add a sensor state associated with a given actor.""" self._logger.info("Sensor state removed for actor '%s'.", actor_id) - del self._sensor_states[actor_id] + return self._sensor_states.pop(actor_id, None) def remove_actor_sensors_by_actor_id( self, actor_id: str, schedule_teardown: bool = True diff --git a/smarts/core/tests/test_vehicle.py b/smarts/core/tests/test_vehicle.py index 630744d150..342cfda8eb 100644 --- a/smarts/core/tests/test_vehicle.py +++ b/smarts/core/tests/test_vehicle.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import math +from functools import partial import numpy as np import pytest @@ -27,13 +28,12 @@ from smarts.core.chassis import BoxChassis from smarts.core.coordinates import Dimensions, Heading, Pose from smarts.core.utils import pybullet -from smarts.core.utils.pybullet import bullet_client as bc from smarts.core.vehicle import VEHICLE_CONFIGS, Vehicle, VehicleState @pytest.fixture def bullet_client(): - client = bc.BulletClient(pybullet.DIRECT) + client = pybullet.SafeBulletClient(pybullet.DIRECT) yield client client.disconnect() @@ -141,3 +141,39 @@ def test_vehicle_bounding_box(bullet_client): vehicle.bounding_box, [[0.5, 2.5], (1.5, 2.5), (1.5, -0.5), (0.5, -0.5)] ): assert np.array_equal(coordinates[0], coordinates[1]) + + +def validate_vehicle(vehicle: Vehicle): + def check_attr(sensor_control_name): + if hasattr(vehicle, sensor_control_name): + sensor_attach = getattr(vehicle, sensor_control_name) + if sensor_attach is None: + return + if isinstance(sensor_attach, property): + sensor_attach = sensor_attach.fget + assert isinstance(sensor_attach, partial) + assert ( + vehicle.id == sensor_attach.keywords["self"].id + ), f"{vehicle.id} | {sensor_attach.keywords['self'].id}" + + for sensor_name in vehicle.sensor_names: + check_attr(f"attach_{sensor_name}") + check_attr(f"detach_{sensor_name}") + + +def test_vehicle_meta_methods(bullet_client): + pose = Pose.from_center((1, 1, 0), Heading(0)) + chassis = BoxChassis( + pose=pose, + speed=0, + dimensions=Dimensions(length=3, width=1, height=1), + bullet_client=bullet_client, + ) + + for i in range(2): + vehicle = Vehicle( + id=f"vehicle-{i}", + chassis=chassis, + vehicle_config_type="passenger", + ) + validate_vehicle(vehicle) diff --git a/smarts/core/utils/class_factory.py b/smarts/core/utils/class_factory.py index 13bbab7a84..7015c8c214 100644 --- a/smarts/core/utils/class_factory.py +++ b/smarts/core/utils/class_factory.py @@ -115,15 +115,15 @@ def find_factory(self, locator): # Import the module so that the agent may register itself in the index # it is assumed that a `register(name=..., entry_point=...)` exists in the target module. module = importlib.import_module(mod_name) - except ImportError: + except ImportError as exc: import sys raise ImportError( f"Ensure that `{mod_name}` module can be found from your " f"PYTHONPATH and name=`{locator}` exists (e.g. was registered " - "manually or downloaded.\n" + "manually or downloaded).\n" f"`PYTHONPATH`: `{sys.path}`" - ) + ) from exc else: # There is no module component. name = mod_name @@ -132,8 +132,8 @@ def find_factory(self, locator): # See if `register()` has been called. # return the builder if it exists. return self.index[name] - except KeyError: - raise NameError(f"Locator not registered in lookup: {locator}") + except KeyError as exc: + raise NameError(f"Locator not registered in lookup: {locator}") from exc def make(self, locator, **kwargs): """Calls the factory with `locator` name key supplying the keyword arguments as argument diff --git a/smarts/core/vehicle.py b/smarts/core/vehicle.py index 831745ced7..32e54659ac 100644 --- a/smarts/core/vehicle.py +++ b/smarts/core/vehicle.py @@ -17,6 +17,8 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import annotations + import importlib.resources as pkg_resources import logging import os @@ -59,7 +61,7 @@ class Vehicle: """Represents a single vehicle.""" _HAS_DYNAMIC_ATTRIBUTES = True # dynamic pytype attribute - _sensor_names = [ + _sensor_names = ( "ogm_sensor", "rgb_sensor", "lidar_sensor", @@ -73,7 +75,7 @@ class Vehicle: "lane_position_sensor", "via_sensor", "signals_sensor", - ] + ) def __init__( self, @@ -262,6 +264,12 @@ def valid(self) -> bool: """Check if the vehicle still `exists` and is still operable.""" return self._initialized + @classmethod + @property + def sensor_names(cls) -> Tuple[str]: + """The names of the sensors that are potentially available to this vehicle.""" + return cls._sensor_names + @staticmethod @lru_cache(maxsize=None) def vehicle_urdf_path(vehicle_type: str, override_path: Optional[str]) -> str: @@ -413,11 +421,12 @@ def build_social_vehicle(sim, vehicle_id, vehicle_state: VehicleState) -> "Vehic dimensions=dims, bullet_client=sim.bc, ) - return Vehicle( + vehicle = Vehicle( id=vehicle_id, chassis=chassis, vehicle_config_type=vehicle_state.vehicle_config_type, ) + return vehicle @classmethod def attach_sensors_to_vehicle( @@ -634,13 +643,16 @@ def attach_sensor(self, sensor, sensor_name): (for example, for observation collection from history vehicles), but if that vehicle gets hijacked, we want to use the sensors specified by the hijacking agent's interface.""" - self.detach_sensor(sensor_name) - self._log.debug("replacing existing %s on vehicle %s", sensor_name, self.id) + detach = getattr(self, f"detach_{sensor_name}") + if detach: + self.detach_sensor(sensor_name) + self._log.debug("Replaced existing %s on vehicle %s", sensor_name, self.id) setattr(self, f"_{sensor_name}", sensor) self._sensors[sensor_name] = sensor def detach_sensor(self, sensor_name): """Detach a sensor by name.""" + self._log.debug("Removed existing %s on vehicle %s", sensor_name, self.id) sensor = getattr(self, f"_{sensor_name}", None) if sensor is not None: setattr(self, f"_{sensor_name}", None) @@ -655,48 +667,61 @@ def subscribed_to(self, sensor_name): def sensor_property(self, sensor_name): """Call a sensor by name.""" sensor = getattr(self, f"_{sensor_name}", None) - assert sensor is not None, f"{sensor_name} is not attached to {self.id}" + assert sensor is not None, f"'{sensor_name}' is not attached to '{self.id}'" return sensor - def _meta_create_sensor_functions(self): - # Bit of metaprogramming to make sensor creation more DRY - sensor_names = self._sensor_names - for sensor_name in sensor_names: - setattr(Vehicle, f"_{sensor_name}", None) + def _meta_create_instance_sensor_functions(self): + for sensor_name in Vehicle._sensor_names: + setattr(self, f"_{sensor_name}", None) setattr( - Vehicle, + self, f"attach_{sensor_name}", - partial(self.attach_sensor, sensor_name=sensor_name), + partial( + self.__class__.attach_sensor, self=self, sensor_name=sensor_name + ), ) setattr( - Vehicle, + self, f"detach_{sensor_name}", - partial(self.detach_sensor, sensor_name=sensor_name), + partial( + self.__class__.detach_sensor, self=self, sensor_name=sensor_name + ), ) + + @classmethod + @lru_cache(1) + def _meta_create_class_sensor_functions(cls): + for sensor_name in cls._sensor_names: setattr( - Vehicle, + cls, f"subscribed_to_{sensor_name}", property( - partial(self.__class__.subscribed_to, sensor_name=sensor_name) + partial(cls.subscribed_to, sensor_name=sensor_name) ), ) setattr( Vehicle, f"{sensor_name}", property( - partial(self.__class__.sensor_property, sensor_name=sensor_name) + partial(cls.sensor_property, sensor_name=sensor_name) ), ) def detach_all_sensors_from_vehicle(vehicle): sensors = [] - for sensor_name in sensor_names: + for sensor_name in cls._sensor_names: detach_sensor_func = getattr(vehicle, f"detach_{sensor_name}") sensors.append(detach_sensor_func()) return sensors setattr( - Vehicle, + cls, "detach_all_sensors_from_vehicle", staticmethod(detach_all_sensors_from_vehicle), ) + + def _meta_create_sensor_functions(self): + # Bit of metaprogramming to make sensor creation more DRY + self._meta_create_instance_sensor_functions() + self._meta_create_class_sensor_functions() + diff --git a/smarts/core/vehicle_index.py b/smarts/core/vehicle_index.py index eada93c587..6e8472756b 100644 --- a/smarts/core/vehicle_index.py +++ b/smarts/core/vehicle_index.py @@ -548,6 +548,8 @@ def relinquish_agent_control( # pytype: disable=attribute-error Vehicle.detach_all_sensors_from_vehicle(vehicle) # pytype: enable=attribute-error + sim.sensor_manager.remove_actor_sensors_by_actor_id(vehicle_id) + sim.sensor_manager.remove_sensor_state_by_actor_id(vehicle_id) vehicle = self._vehicles[v_id] box_chassis = BoxChassis( From fb3480eceade5884fd81add33e5f582d0fa84a85 Mon Sep 17 00:00:00 2001 From: Tucker Date: Sat, 6 Jan 2024 13:02:29 -0500 Subject: [PATCH 4/6] Put sensor manager logging back to default. --- smarts/core/sensor_manager.py | 9 ++++----- smarts/core/vehicle.py | 9 ++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/smarts/core/sensor_manager.py b/smarts/core/sensor_manager.py index 521528072f..c00c323245 100644 --- a/smarts/core/sensor_manager.py +++ b/smarts/core/sensor_manager.py @@ -44,7 +44,6 @@ class SensorManager: def __init__(self): self._logger = logging.getLogger(self.__class__.__name__) - self._logger.setLevel(logging.INFO) self._sensors: Dict[str, Sensor] = {} # {actor_id: } @@ -179,12 +178,12 @@ def teardown(self, renderer): def add_sensor_state(self, actor_id: str, sensor_state: SensorState): """Add a sensor state associated with a given actor.""" - self._logger.info("Sensor state added for actor '%s'.", actor_id) + self._logger.debug("Sensor state added for actor '%s'.", actor_id) self._sensor_states[actor_id] = sensor_state def remove_sensor_state_by_actor_id(self, actor_id: str): """Add a sensor state associated with a given actor.""" - self._logger.info("Sensor state removed for actor '%s'.", actor_id) + self._logger.debug("Sensor state removed for actor '%s'.", actor_id) return self._sensor_states.pop(actor_id, None) def remove_actor_sensors_by_actor_id( @@ -203,7 +202,7 @@ def remove_actor_sensors_by_actor_id( sensors_by_actor = self._sensors_by_actor_id.get(actor_id) if not sensors_by_actor: return [] - self._logger.info("Target sensor removal for actor '%s'.", actor_id) + self._logger.debug("Target sensor removal for actor '%s'.", actor_id) discarded_sensors = [] for sensor_id in sensors_by_actor: self._actors_by_sensor_id[sensor_id].remove(actor_id) @@ -219,7 +218,7 @@ def remove_sensor( self, sensor_id: str, schedule_teardown: bool = False ) -> Optional[Sensor]: """Remove a sensor by its id. Removes any associations it has with actors.""" - self._logger.info("Target removal of sensor '%s'.", sensor_id) + self._logger.debug("Target removal of sensor '%s'.", sensor_id) sensor = self._sensors.get(sensor_id) if not sensor: return None diff --git a/smarts/core/vehicle.py b/smarts/core/vehicle.py index 32e54659ac..dab6dde1cb 100644 --- a/smarts/core/vehicle.py +++ b/smarts/core/vehicle.py @@ -695,16 +695,12 @@ def _meta_create_class_sensor_functions(cls): setattr( cls, f"subscribed_to_{sensor_name}", - property( - partial(cls.subscribed_to, sensor_name=sensor_name) - ), + property(partial(cls.subscribed_to, sensor_name=sensor_name)), ) setattr( Vehicle, f"{sensor_name}", - property( - partial(cls.sensor_property, sensor_name=sensor_name) - ), + property(partial(cls.sensor_property, sensor_name=sensor_name)), ) def detach_all_sensors_from_vehicle(vehicle): @@ -724,4 +720,3 @@ def _meta_create_sensor_functions(self): # Bit of metaprogramming to make sensor creation more DRY self._meta_create_instance_sensor_functions() self._meta_create_class_sensor_functions() - From d2aacdec513d0607b5b13d9546829e657555c229 Mon Sep 17 00:00:00 2001 From: Tucker Date: Sat, 6 Jan 2024 13:04:26 -0500 Subject: [PATCH 5/6] Update changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e481b9f2c..f0f8409083 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Copy and pasting the git commit messages is __NOT__ enough. - Fixed an issue where the sensor state could be `None` when calling `SMARTS.observe_from()` on a non-ego vehicle. See #2133. - The via sensor and trip meter sensor now work without a mission. - Fixed a bug with `VehicleIndex.attach_sensors_to_vehicle()` that would generate an invalid plan. +- Fixed a bug where vehicle sensor meta attributes would reference the wrong vehicle. ### Removed ### Security From dfd18b38bfee5cb7cbf7466ebd3c18deeacda03b Mon Sep 17 00:00:00 2001 From: Tucker Date: Sat, 6 Jan 2024 18:48:23 -0500 Subject: [PATCH 6/6] Fix last of tests. --- smarts/core/agent_interface.py | 6 ++---- smarts/core/tests/test_vehicle.py | 4 ++-- smarts/core/vehicle.py | 15 ++++++--------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/smarts/core/agent_interface.py b/smarts/core/agent_interface.py index ee7b640756..1f974734cb 100644 --- a/smarts/core/agent_interface.py +++ b/smarts/core/agent_interface.py @@ -268,12 +268,10 @@ class AgentInterface: debug: bool = False """Enable debug information for the various sensors and action spaces.""" - event_configuration: EventConfiguration = field( - default_factory=lambda: EventConfiguration() - ) + event_configuration: EventConfiguration = field(default_factory=EventConfiguration) """Configurable criteria of when to trigger events""" - done_criteria: DoneCriteria = field(default_factory=lambda: DoneCriteria()) + done_criteria: DoneCriteria = field(default_factory=DoneCriteria) """Configurable criteria of when to mark this actor as done. Done actors will be removed from the environment and may trigger the episode to be done.""" diff --git a/smarts/core/tests/test_vehicle.py b/smarts/core/tests/test_vehicle.py index 342cfda8eb..56cfd8d2c3 100644 --- a/smarts/core/tests/test_vehicle.py +++ b/smarts/core/tests/test_vehicle.py @@ -153,8 +153,8 @@ def check_attr(sensor_control_name): sensor_attach = sensor_attach.fget assert isinstance(sensor_attach, partial) assert ( - vehicle.id == sensor_attach.keywords["self"].id - ), f"{vehicle.id} | {sensor_attach.keywords['self'].id}" + vehicle.id == sensor_attach.func.__self__.id + ), f"{vehicle.id} | {sensor_attach.func.__self__.id}" for sensor_name in vehicle.sensor_names: check_attr(f"attach_{sensor_name}") diff --git a/smarts/core/vehicle.py b/smarts/core/vehicle.py index dab6dde1cb..e8b5d811a5 100644 --- a/smarts/core/vehicle.py +++ b/smarts/core/vehicle.py @@ -264,11 +264,10 @@ def valid(self) -> bool: """Check if the vehicle still `exists` and is still operable.""" return self._initialized - @classmethod @property - def sensor_names(cls) -> Tuple[str]: + def sensor_names(self) -> Tuple[str]: """The names of the sensors that are potentially available to this vehicle.""" - return cls._sensor_names + return self._sensor_names @staticmethod @lru_cache(maxsize=None) @@ -467,6 +466,7 @@ def add_sensor_if_needed( vehicle.attach_sensor(sensor, sensor_name) added_sensors.append((sensor_name, sensor)) + # pytype: disable=attribute-error add_sensor_if_needed(TripMeterSensor, sensor_name="trip_meter_sensor") add_sensor_if_needed(DrivenPathSensor, sensor_name="driven_path_sensor") if agent_interface.neighborhood_vehicle_states: @@ -555,6 +555,7 @@ def add_sensor_if_needed( "signals_sensor", lookahead=agent_interface.signals.lookahead, ) + # pytype: enable=attribute-error for sensor_name, sensor in added_sensors: if not sensor: @@ -676,16 +677,12 @@ def _meta_create_instance_sensor_functions(self): setattr( self, f"attach_{sensor_name}", - partial( - self.__class__.attach_sensor, self=self, sensor_name=sensor_name - ), + partial(self.attach_sensor, sensor_name=sensor_name), ) setattr( self, f"detach_{sensor_name}", - partial( - self.__class__.detach_sensor, self=self, sensor_name=sensor_name - ), + partial(self.detach_sensor, sensor_name=sensor_name), ) @classmethod