Skip to content

Commit aa5ee7d

Browse files
authored
Merge pull request #116 from ARRC-Rocket/develop
REL: complete v0.1.0 with the ActiveRocketPy bump and the changelog section
2 parents 69dfbda + 915b09a commit aa5ee7d

4 files changed

Lines changed: 338 additions & 17 deletions

File tree

BalloonPoppingGymEnv/envs/balloon_world.py

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,81 @@
3030

3131
logger = logging.getLogger(__name__)
3232

33+
# How many numbers each action field holds. The environment indexes `tvc` and
34+
# `launch_inclination_heading` by position, so a wrong count is not usable
35+
# either, and an empty array passes a finiteness test on its own.
36+
ACTION_FIELD_SIZES = {
37+
"launch_inclination_heading": 2,
38+
"tvc": 2,
39+
"roll": 1,
40+
"throttle": 1,
41+
}
42+
43+
44+
# Which repetition of an unusable action gets a log line. A broken policy stays
45+
# broken, and one line per step for the rest of the horizon is its own problem.
46+
_UNUSABLE_ACTION_LOG_STEPS = frozenset({1, 10, 100, 1000})
47+
48+
49+
def _is_launch_command(action):
50+
"""Whether `action["launch"]` asks for a launch, without guessing.
51+
52+
`bool(np.nan)` is True and a two element array raises, so the truthiness of
53+
whatever arrives is not a safe question to ask directly.
54+
"""
55+
try:
56+
values = np.asarray(action["launch"]).reshape(-1)
57+
except (KeyError, TypeError, ValueError):
58+
return False
59+
if values.size != 1:
60+
return False
61+
try:
62+
value = float(values[0])
63+
except (TypeError, ValueError, OverflowError):
64+
return False
65+
# Finite first: `nan != 0` is True, so a NaN would otherwise launch.
66+
return bool(np.isfinite(value)) and value != 0.0
67+
68+
69+
def check_action(action):
70+
"""The action fields the environment cannot use, by name.
71+
72+
A policy that diverges emits NaN, and it is the most ordinary failure there
73+
is while training one. An empty list means every field is usable.
74+
75+
Public so an agent can ask the same question before returning an action:
76+
77+
if check_action(action):
78+
action = last_good_action
79+
"""
80+
return sorted(_usable_action_fields(action)[1])
81+
82+
83+
def _usable_action_fields(action):
84+
"""The fields as arrays of the right size, and the names of the rest.
85+
86+
Validating a converted copy and then handing the original to the actuators
87+
would check something other than what is used: a scalar `tvc` converts to a
88+
finite array and then fails at `action["tvc"][0]`. So the converted values
89+
are what the caller gets.
90+
"""
91+
usable = {}
92+
unusable = set()
93+
for field, size in ACTION_FIELD_SIZES.items():
94+
if field not in action:
95+
unusable.add(field)
96+
continue
97+
try:
98+
values = np.atleast_1d(np.asarray(action[field], dtype=float))
99+
except (TypeError, ValueError, OverflowError):
100+
unusable.add(field)
101+
continue
102+
if values.size != size or not np.all(np.isfinite(values)):
103+
unusable.add(field)
104+
continue
105+
usable[field] = values
106+
return usable, unusable
107+
33108

34109
def _remove_monte_carlo_workspace(directory):
35110
"""Delete a Monte Carlo scratch directory, saying so if it will not go.
@@ -148,6 +223,8 @@ def __init__(self, render_mode, parameters):
148223
self._balloon_states = np.array(np.zeros((self.balloon_parameters["num"], 6)))
149224
# (gyroX, gyroY, gyroZ, accX, accY, accZ, posX, posY, posZ, velX, velY, velZ)
150225
self._rocket_sensors = np.full(12, np.nan)
226+
# Steps whose action the environment could not use, for rate limiting.
227+
self._unusable_action_steps = 0
151228
# (posX, posY, posZ, velX, velY, velZ, e0, e1, e2, e3, w1, w2, w3)
152229
self._rocket_states = np.full(13, np.nan)
153230
# Where the next pop sweep starts. Tracked separately from
@@ -294,6 +371,8 @@ def reset(self, seed=None, options=None):
294371

295372
self._balloon_states = self._balloon_flights[:, :, 0]
296373
self._rocket_sensors = np.full(12, np.nan)
374+
# Steps whose action the environment could not use, for rate limiting.
375+
self._unusable_action_steps = 0
297376
self._rocket_states = np.full(13, np.nan)
298377
self._sweep_origin = None
299378
self.trajectories = None
@@ -325,28 +404,53 @@ def step(self, action):
325404
ground_mask = self._balloon_status[:, 0] == 0
326405
self._balloon_status[released_mask & ground_mask, 0] = 1
327406

407+
# A diverging policy emits NaN, and it is the most ordinary failure there
408+
# is while training one. A step it cannot command is not a run it should
409+
# lose, so the command is dropped and the episode carries on.
410+
commands, unusable = _usable_action_fields(action)
411+
if unusable:
412+
# Rate limited, since a policy that breaks stays broken and one line
413+
# per step for the rest of the horizon is its own denial of service.
414+
self._unusable_action_steps += 1
415+
if self._unusable_action_steps in _UNUSABLE_ACTION_LOG_STEPS:
416+
logger.warning(
417+
"Step %d: ignoring %s, which the environment cannot use "
418+
"(%d such steps so far)",
419+
self.current_step,
420+
", ".join(sorted(unusable)),
421+
self._unusable_action_steps,
422+
)
423+
328424
if not self.rocket_launched:
329425
_rocket_finished = False
330-
if action["launch"]: # Init rocket flight with first launch action
331-
self.rocket_launched = True
332-
self.__get_init_rocket_states(
333-
action["launch_inclination_heading"][0],
334-
action["launch_inclination_heading"][1],
335-
)
426+
# Refused rather than dropped, because there is no previous attitude
427+
# to fall back on. The agent can launch on any later step.
428+
if _is_launch_command(action) and "launch_inclination_heading" in commands:
429+
attitude = commands["launch_inclination_heading"]
430+
self.__get_init_rocket_states(attitude[0], attitude[1])
336431
self.initial_solution[0] = (
337432
self.current_step * self.simulation_parameters["time_step"]
338433
)
339434
self.__init_rocket_simulation()
340435
self._sweep_origin = np.asarray(self.initial_solution[1:4], dtype=float)
436+
# Last, so a failure above leaves the environment unlaunched
437+
# rather than launched with no flight for the next step to use.
438+
self.rocket_launched = True
341439
else: # Apply action to step the rocket simulation and get sensor measurements
342-
self._rocket_flight.rocket.roll_control.roll_torque = action["roll"]
343-
self._rocket_flight.rocket.thrust_vector_control.gimbal_angle_x = action[
344-
"tvc"
345-
][0]
346-
self._rocket_flight.rocket.thrust_vector_control.gimbal_angle_y = action[
347-
"tvc"
348-
][1]
349-
self._rocket_flight.rocket.throttle_control.throttle = action["throttle"]
440+
# Each actuator keeps the value it already holds when its field is
441+
# dropped, which is what a real one does between commands.
442+
if "roll" in commands:
443+
self._rocket_flight.rocket.roll_control.roll_torque = float(
444+
commands["roll"][0]
445+
)
446+
if "tvc" in commands:
447+
thrust_vector = self._rocket_flight.rocket.thrust_vector_control
448+
thrust_vector.gimbal_angle_x = float(commands["tvc"][0])
449+
thrust_vector.gimbal_angle_y = float(commands["tvc"][1])
450+
if "throttle" in commands:
451+
self._rocket_flight.rocket.throttle_control.throttle = float(
452+
commands["throttle"][0]
453+
)
350454
self._rocket_flight.step_simulation()
351455
_sensor = self._rocket_flight.sensors
352456
self._rocket_sensors[:3] = _sensor[0].measurement # gyro

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ to the pull request that made the change.
1010

1111
## [Unreleased]
1212

13-
*Changes on `develop` since v0.0.2. The release PR renames this section.*
13+
*Changes on `develop` since v0.1.0.*
14+
15+
## [0.1.0] - 2026-07-29
16+
17+
Released as [v0.1.0](https://github.com/ARRC-Rocket/BalloonPoppingChallenge/releases/tag/v0.1.0)
18+
in #61, folding in the work from #39 through #111.
1419

1520
### Added
1621

@@ -148,6 +153,7 @@ in #33, folding in the work from #5 through #34.
148153
First public release: the Gymnasium environment, the example agents, scenarios 0
149154
and 1, and the Colab example (#1, #3, #4).
150155

151-
[Unreleased]: https://github.com/ARRC-Rocket/BalloonPoppingChallenge/compare/v0.0.2...develop
156+
[Unreleased]: https://github.com/ARRC-Rocket/BalloonPoppingChallenge/compare/v0.1.0...develop
157+
[0.1.0]: https://github.com/ARRC-Rocket/BalloonPoppingChallenge/compare/v0.0.2...v0.1.0
152158
[0.0.2]: https://github.com/ARRC-Rocket/BalloonPoppingChallenge/compare/v0.0.1...v0.0.2
153159
[0.0.1]: https://github.com/ARRC-Rocket/BalloonPoppingChallenge/releases/tag/v0.0.1

0 commit comments

Comments
 (0)