Skip to content

Commit b985131

Browse files
authored
Merge pull request #44 from jjshoots/staggered_render
Staggered camera capture for drones
2 parents c9488c7 + b7796cc commit b985131

File tree

85 files changed

+2905
-2727
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+2905
-2727
lines changed

PyFlyt/core/abstractions/base_drone.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,15 @@ def reset(self) -> None:
155155
raise NotImplementedError
156156

157157
@abstractmethod
158-
def update_control(self) -> None:
158+
def update_control(self, physics_step: int) -> None:
159159
"""Updates onboard flight control laws at a rate specified by `control_hz`.
160160
161161
Example Implementation:
162-
>>> def update_control(self):
162+
>>> def update_control(self, physics_step: int) -> None:
163+
>>> # skip control if we have not enough physics steps
164+
>>> if physics_step % self.physics_control_ratio != 0:
165+
>>> return
166+
>>>
163167
>>> # depending on the flight control mode, do things
164168
>>> if self.mode == 0:
165169
>>> # assign the actuator commands to be some function of the setpoint
@@ -182,7 +186,7 @@ def update_physics(self) -> None:
182186
"""Updates all physics on the vehicle at a rate specified by `physics_hz`.
183187
184188
Example Implementation:
185-
>>> def update_physics(self):
189+
>>> def update_physics(self) -> None:
186190
>>> self.lifting_surfaces.physics_update(self.cmd[...])
187191
>>> self.boosters.physics_update(self.cmd[...])
188192
>>> ...
@@ -194,7 +198,7 @@ def update_state(self) -> None:
194198
"""Updates the vehicle's state values at a rate specified by `phyiscs_hz`.
195199
196200
Example Implementation:
197-
>>> def update_state(self):
201+
>>> def update_state(self) -> None:
198202
>>> # get all relevant information from PyBullet backend
199203
>>> lin_pos, ang_pos = self.p.getBasePositionAndOrientation(self.Id)
200204
>>> lin_vel, ang_vel = self.p.getBaseVelocity(self.Id)
@@ -222,12 +226,13 @@ def update_state(self) -> None:
222226
raise NotImplementedError
223227

224228
@abstractmethod
225-
def update_last(self) -> None:
229+
def update_last(self, physics_step: int) -> None:
226230
"""Update that happens at the end of `Aviary.step()`, usually reserved for camera updates.
227231
228232
Example Implementation:
229-
>>> def update_last(self):
230-
>>> if self.use_camera:
233+
>>> def update_last(self, physics_step: int) -> None:
234+
>>>
235+
>>> if self.use_camera and (physics_step % self.physics_camera_ratio == 0):
231236
>>> self.rgbaImg, self.depthImg, self.segImg = self.camera.capture_image()
232237
"""
233238
raise NotImplementedError

PyFlyt/core/aviary.py

Lines changed: 60 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@
1717
DroneIndex = int
1818

1919

20+
class AviaryInitException(Exception):
21+
def __init__(self, message: str) -> None:
22+
self.message = message
23+
super().__init__(self.message)
24+
25+
def __str__(self) -> str:
26+
return f"Aviary Error: {self.message}"
27+
28+
2029
class Aviary(bullet_client.BulletClient):
2130
"""Aviary class, the core of how PyFlyt handles UAVs in the PyBullet simulation environment.
2231
@@ -73,15 +82,18 @@ def __init__(
7382
print("\033[A \033[A")
7483

7584
# check for starting position and orientation shapes
76-
assert (
77-
len(start_pos.shape) == 2
78-
), f"start_pos must be shape (n, 3), currently {start_pos.shape}."
79-
assert (
80-
start_pos.shape[-1] == 3
81-
), f"start_pos must be shape (n, 3), currently {start_pos.shape}."
82-
assert (
83-
start_orn.shape == start_pos.shape
84-
), f"start_orn must be same shape as start_pos, currently {start_orn.shape}."
85+
if len(start_pos.shape) != 2:
86+
raise AviaryInitException(
87+
f"start_pos must be shape (n, 3), currently {start_pos.shape}."
88+
)
89+
if start_pos.shape[-1] != 3:
90+
raise AviaryInitException(
91+
f"start_pos must be shape (n, 3), currently {start_pos.shape}."
92+
)
93+
if start_orn.shape != start_pos.shape:
94+
raise AviaryInitException(
95+
f"start_orn must be same shape as start_pos, currently {start_orn.shape}."
96+
)
8597

8698
# check the physics hz
8799
if physics_hz != 240.0:
@@ -91,27 +103,33 @@ def __init__(
91103

92104
# check to ensure drone type has same number as drones if is list/tuple
93105
if isinstance(drone_type, (tuple, list)):
94-
assert (
95-
len(drone_type) == start_pos.shape[0]
96-
), f"If multiple `drone_types` are used, must have same number of `drone_types` ({len(drone_type)}) as number of drones ({start_pos.shape[0]})."
106+
if len(drone_type) != start_pos.shape[0]:
107+
raise AviaryInitException(
108+
f"If multiple `drone_types` are used, must have same number of `drone_types` ({len(drone_type)}) as number of drones ({start_pos.shape[0]})."
109+
)
110+
if len(drone_type) != start_pos.shape[0]:
111+
raise AviaryInitException(
112+
f"If multiple `drone_types` are used, must have same number of `drone_types` ({len(drone_type)}) as number of drones ({start_pos.shape[0]})."
113+
)
97114
# check to ensure drone type has same number as drones if is list/tuple
98115
if isinstance(drone_options, (tuple, list)):
99-
assert (
100-
len(drone_options) == start_pos.shape[0]
101-
), f"If multiple `drone_options` ({len(drone_options)}) are used, must have same number of `drone_options` as number of drones ({start_pos.shape[0]})."
116+
if len(drone_options) != start_pos.shape[0]:
117+
raise AviaryInitException(
118+
f"If multiple `drone_options` ({len(drone_options)}) are used, must have same number of `drone_options` as number of drones ({start_pos.shape[0]})."
119+
)
102120

103121
# constants
104-
self.num_drones = start_pos.shape[0]
105-
self.start_pos = start_pos
106-
self.start_orn = start_orn
122+
self.num_drones: int = start_pos.shape[0]
123+
self.start_pos: np.ndarray = start_pos
124+
self.start_orn: np.ndarray = start_orn
107125

108126
# do not change because pybullet doesn't like it
109127
# default physics looprate is 240 Hz
110-
self.physics_hz = physics_hz
111-
self.physics_period = 1.0 / physics_hz
128+
self.physics_hz: int = physics_hz
129+
self.physics_period: float = 1.0 / physics_hz
112130

113131
# mapping of drone type string to the constructors
114-
self.drone_type_mappings = dict()
132+
self.drone_type_mappings: dict[str, type[DroneClass]] = dict()
115133
self.drone_type_mappings["quadx"] = QuadX
116134
self.drone_type_mappings["fixedwing"] = Fixedwing
117135
self.drone_type_mappings["rocket"] = Rocket
@@ -123,18 +141,24 @@ def __init__(
123141

124142
# store all drone types
125143
if isinstance(drone_type, (tuple, list)):
126-
assert all(
127-
dt in self.drone_type_mappings for dt in drone_type
128-
), f"One of types in `drone_type` {drone_type} is not amongst known types {self.drone_type_mappings.keys()}."
144+
if not all(dt in self.drone_type_mappings for dt in drone_type):
145+
raise AviaryInitException(
146+
f"One of types in `drone_type` {drone_type} is not amongst known types {self.drone_type_mappings.keys()}."
147+
)
148+
if not all(dt in self.drone_type_mappings for dt in drone_type):
149+
raise AviaryInitException(
150+
f"One of types in `drone_type` {drone_type} is not amongst known types {self.drone_type_mappings.keys()}."
151+
)
129152
self.drone_type = drone_type
130153
else:
131-
assert (
132-
drone_type in self.drone_type_mappings
133-
), f"Can't find `drone_type` {drone_type} amongst known types {self.drone_type_mappings.keys()}."
154+
if drone_type not in self.drone_type_mappings:
155+
raise AviaryInitException(
156+
f"Can't find `drone_type` {drone_type} amongst known types {self.drone_type_mappings.keys()}."
157+
)
134158
self.drone_type = repeat(drone_type)
135159

136160
# store the drone options
137-
if isinstance(drone_options, (tuple, list)):
161+
if isinstance(drone_options, Sequence):
138162
self.drone_options = drone_options
139163
else:
140164
self.drone_options = repeat(drone_options)
@@ -251,7 +275,7 @@ def reset(self, seed: None | int = None) -> None:
251275
# reset all drones and initialize required states
252276
[drone.reset() for drone in self.drones]
253277
[drone.update_state() for drone in self.drones]
254-
[drone.update_last() for drone in self.drones]
278+
[drone.update_last(0) for drone in self.drones]
255279

256280
def register_all_new_bodies(self) -> None:
257281
"""Registers all new bodies in the environment to be able to handle collisions later.
@@ -441,21 +465,18 @@ def step(self) -> None:
441465
self.contact_array &= False
442466

443467
# step the environment enough times for one control loop of the slowest controller
444-
for step in range(self.updates_per_step):
445-
# update onboard avionics conditionally
446-
[
447-
drone.update_control()
448-
for drone in self.armed_drones
449-
if step % drone.physics_control_ratio == 0
450-
]
451-
452-
# update physics and state
468+
for _ in range(self.updates_per_step):
469+
# update control and physics
470+
[drone.update_control(self.physics_steps) for drone in self.armed_drones]
453471
[drone.update_physics() for drone in self.armed_drones]
454-
[drone.update_state() for drone in self.armed_drones]
455472

456473
# advance pybullet
457474
self.stepSimulation()
458475

476+
# update states and camera
477+
[drone.update_state() for drone in self.armed_drones]
478+
[drone.update_last(self.physics_steps) for drone in self.armed_drones]
479+
459480
# splice out collisions
460481
for collision in self.getContactPoints():
461482
self.contact_array[collision[1], collision[2]] = True
@@ -465,7 +486,4 @@ def step(self) -> None:
465486
self.physics_steps += 1
466487
self.elapsed_time = self.physics_steps / self.physics_hz
467488

468-
# update the last components of the drones, this is usually limited to cameras only
469-
[drone.update_last() for drone in self.armed_drones]
470-
471489
self.aviary_steps += 1

PyFlyt/core/drones/fixedwing.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def __init__(
3030
camera_FOV_degrees: int = 90,
3131
camera_resolution: tuple[int, int] = (128, 128),
3232
camera_position_offset: np.ndarray = np.array([-3.0, 0.0, 1.0]),
33+
camera_fps: None | int = None,
3334
starting_velocity: np.ndarray = np.array([20.0, 0.0, 0.0]),
3435
):
3536
"""Creates a Fixedwing UAV and handles all relevant control and physics.
@@ -49,6 +50,7 @@ def __init__(
4950
camera_FOV_degrees (int): camera_FOV_degrees
5051
camera_resolution (tuple[int, int]): camera_resolution
5152
camera_position_offset (np.ndarray): offset position of the camera
53+
camera_fps (None | int): camera_fps
5254
starting_velocity (np.ndarray): vector representing the velocity at spawn
5355
"""
5456
super().__init__(
@@ -178,6 +180,15 @@ def __init__(
178180
is_tracking_camera=True,
179181
)
180182

183+
# compute camera fps parameters
184+
if camera_fps:
185+
assert (
186+
(physics_hz / camera_fps) % 1 == 0
187+
), f"Expected `camera_fps` to roundly divide `physics_hz`, got {camera_fps=} and {physics_hz=}."
188+
self.physics_camera_ratio = int(physics_hz / camera_fps)
189+
else:
190+
self.physics_camera_ratio = 1
191+
181192
def reset(self) -> None:
182193
"""Resets the vehicle to the initial state."""
183194
self.set_mode(0)
@@ -212,8 +223,16 @@ def set_mode(self, mode) -> None:
212223
elif mode == 0:
213224
self.setpoint = np.zeros(4)
214225

215-
def update_control(self) -> None:
216-
"""Runs through controllers."""
226+
def update_control(self, physics_step: int) -> None:
227+
"""Runs through controllers.
228+
229+
Args:
230+
physics_step (int): the current physics step
231+
"""
232+
# skip control if we don't have enough physics steps
233+
if physics_step % self.physics_control_ratio != 0:
234+
return
235+
217236
# full control over all surfaces
218237
if self.mode == -1:
219238
self.cmd = self.setpoint
@@ -267,7 +286,11 @@ def update_state(self) -> None:
267286
(self.lifting_surfaces.get_states(), self.motors.get_states())
268287
)
269288

270-
def update_last(self) -> None:
271-
"""Updates things only at the end of `Aviary.step()`."""
272-
if self.use_camera:
289+
def update_last(self, physics_step: int) -> None:
290+
"""Updates things only at the end of `Aviary.step()`.
291+
292+
Args:
293+
physics_step (int): the current physics step
294+
"""
295+
if self.use_camera and (physics_step % self.physics_camera_ratio == 0):
273296
self.rgbaImg, self.depthImg, self.segImg = self.camera.capture_image()

PyFlyt/core/drones/quadx.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def __init__(
3333
camera_angle_degrees: int = 20,
3434
camera_FOV_degrees: int = 90,
3535
camera_resolution: tuple[int, int] = (128, 128),
36+
camera_fps: None | int = None,
3637
):
3738
"""Creates a drone in the QuadX configuration and handles all relevant control and physics.
3839
@@ -50,6 +51,7 @@ def __init__(
5051
camera_angle_degrees (int): camera_angle_degrees
5152
camera_FOV_degrees (int): camera_FOV_degrees
5253
camera_resolution (tuple[int, int]): camera_resolution
54+
camera_fps (None | int): camera_fps
5355
"""
5456
super().__init__(
5557
p=p,
@@ -203,6 +205,15 @@ def __init__(
203205
camera_resolution=camera_resolution,
204206
)
205207

208+
# compute camera fps parameters
209+
if camera_fps:
210+
assert (
211+
(physics_hz / camera_fps) % 1 == 0
212+
), f"Expected `camera_fps` to roundly divide `physics_hz`, got {camera_fps=} and {physics_hz=}."
213+
self.physics_camera_ratio = int(physics_hz / camera_fps)
214+
else:
215+
self.physics_camera_ratio = 1
216+
206217
def reset(self) -> None:
207218
"""Resets the vehicle to the initial state."""
208219
self.set_mode(0)
@@ -380,8 +391,16 @@ def register_controller(
380391
self.registered_controllers[controller_id] = controller_constructor
381392
self.registered_base_modes[controller_id] = base_mode
382393

383-
def update_control(self) -> None:
384-
"""Runs through controllers."""
394+
def update_control(self, physics_step: int) -> None:
395+
"""Runs through controllers.
396+
397+
Args:
398+
physics_step (int): the current physics step
399+
"""
400+
# skip control if we don't have enough physics steps
401+
if physics_step % self.physics_control_ratio != 0:
402+
return
403+
385404
# this is the thing we cascade down controllers
386405
a_output = self.setpoint[:3].copy()
387406
z_output = self.setpoint[-1].copy()
@@ -507,7 +526,11 @@ def update_state(self) -> None:
507526
# update auxiliary information
508527
self.aux_state = self.motors.get_states()
509528

510-
def update_last(self) -> None:
511-
"""Updates things only at the end of `Aviary.step()`."""
512-
if self.use_camera:
529+
def update_last(self, physics_step: int) -> None:
530+
"""Updates things only at the end of `Aviary.step()`.
531+
532+
Args:
533+
physics_step (int): the current physics step
534+
"""
535+
if self.use_camera and (physics_step % self.physics_camera_ratio == 0):
513536
self.rgbaImg, self.depthImg, self.segImg = self.camera.capture_image()

0 commit comments

Comments
 (0)