Code reference¶
Auto-generated from source docstrings. For the conceptual contract see Core concepts — the environment contract, the backups, and the MAP naming law.
The module layout follows MAP: buffers carry the Mode, the *_1p / *_2p
modules carry the Players, and the *_base modules hold what both player counts
of an Algorithm share.
Backups — the value operators¶
safety_sb3.backups ¶
The Bellman backups — defined ONCE, used by every learner.
This library solves two different safety problems, and they take different value
operators. Before this module existed the operators were re-implemented at four
call sites (the numpy rollout buffer, the torch rollout buffer, and two SAC
train() bodies); they silently diverged, and the PPO family spent the whole
2026 campaign optimizing a fixed point that was neither problem's value. Every
backup now routes through here so that cannot recur.
A learner is then written without reference to a particular operator — it
takes a mode and calls :func:target. Ordinary sum-of-rewards RL is one of
the modes (:data:CUMULATIVE), so it is a special case of the same code, not a
separate library.
Convention (throughout the library)::
g(s) >= 0 <=> s is OUTSIDE the failure set ("safe")
l(s) >= 0 <=> s is INSIDE the target set ("reached")
V is MAXIMIZED; V(s) >= 0 <=> s is in the solution set
The operators¶
Avoid (stay safe forever) — Fisac et al. 2019; ISAACS eq. 6/7::
V(s) = (1 - gamma) * g(s) + gamma * min( g(s), V(s') )
Reach-avoid (reach the target while staying safe throughout) — Hsu et al. RSS'21 eq. 15; Gameplay Filters eq. 6a::
V(s) = (1 - gamma) * min(l(s), g(s))
+ gamma * min( g(s), max( l(s), V(s') ) )
Cumulative (the STANDARD RL operator — maximize the discounted sum of rewards; Bellman 1957, and every textbook since)::
V(s) = r(s) + gamma * V(s')
This one is not a safety operator and does not follow the sign convention
above: its first argument is a reward, not a margin, V is an expected
discounted return with no zero-level-set meaning, and there is no g, no
l and no failure set. It is here because the learners in this library are
written against :func:target rather than against a fixed backup, so plain
sum-of-rewards RL costs one branch and comes out as the degenerate member of the
family. Use it to run a nominal (reward-maximizing) baseline through exactly
the same actor/critic/entropy code as the safety learners — the standard control
for "is the safety operator doing the work, or is it just SAC?". Do not read a
cumulative V as a safety certificate; V >= 0 means nothing here.
The two safety operators share the shape (1 - gamma) * anchor + gamma * backup. The anchor is
the "episode terminates now" payoff — 1 - gamma is the per-step
termination probability of the discounted formulation, so the anchor is what the
trajectory scores if it stops here:
- avoid: stopping now is a win iff you are safe ->
g - reach-avoid: iff you are in the target AND safe ->
min(l, g)
The reach-avoid anchor is the same expression as the finite-horizon terminal
condition V_H = min(l, g) (Gameplay Filters eq. 5b); that identity is the
structural tell that it is not a stylistic choice.
Why the anchors are not interchangeable¶
Anchoring reach-avoid on g makes "stay safe forever, never reach" a fixed
point at V = g > 0 — a win — when the reach-avoid value of such a trajectory
is max_t min(l_t, min_{s<=t} g_s) = max_t l_t < 0, a loss. The resulting
fixed point is neither problem's value; RSS'21's under-approximation theorem
(RA_gamma nested inside RA) stops applying, so the critic can wrongly
certify reachability and is unsound to filter with. RSS'21 says of the
g-anchored form (its eq. 13) that it approximates "safety or liveness problems,
but not both".
Note that ISAACS anchors on g and is correct to: it is a pure avoid game
with no target set and no l anywhere in the paper. Gameplay Filters extends
ISAACS to reach-avoid and changes the anchor when it does. The mixture — a g
anchor with a max(l, V') recursion — appears in none of the papers.
Avoid is not expressible as a reach-avoid instance¶
Tempting, and wrong: to make the reach-avoid operator compute the avoid value
you would need the anchor to reduce (min(l, g) = g, i.e. l >= g) and
the recursion to reduce (max(l, V') = V', i.e. l <= V'). Since the avoid
recursion caps V' <= g, that needs l >= g >= V' >= l — satisfiable only
in the degenerate case. A large negative l buys the recursion and destroys
the anchor (V == l everywhere, empty safe set); a large positive l buys
the anchor and destroys the recursion (V == g everywhere, no lookahead —
coming failures never propagate). Use the avoid operator for avoid problems.
All functions here are elementwise and shape-agnostic, and accept either numpy
arrays or torch tensors (not_done may be float 0/1 or bool-as-float).
avoid_target ¶
avoid_target(g, v_next, not_done, gamma)
Avoid (safety) target: (1-g)*g + g*min(g, V'); terminal -> g.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
g
|
Array
|
safety margin |
required |
v_next
|
Array
|
bootstrap value |
required |
not_done
|
Array
|
1.0 on non-terminal steps, 0.0 on terminal steps. |
required |
gamma
|
float
|
discount. |
required |
Source code in safety_sb3/backups.py
134 135 136 137 138 139 140 141 142 143 144 145 146 | |
reach_avoid_target ¶
reach_avoid_target(
g, l, v_next, not_done, gamma, terminal_type="all"
)
Reach-avoid target: (1-g)*min(l,g) + g*min(g, max(l, V')).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
g
|
Array
|
safety margin |
required |
l
|
Array
|
target margin |
required |
v_next
|
Array
|
bootstrap value |
required |
not_done
|
Array
|
1.0 on non-terminal steps, 0.0 on terminal steps. |
required |
gamma
|
float
|
discount. |
required |
terminal_type
|
str
|
target at terminal steps. |
'all'
|
Source code in safety_sb3/backups.py
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
cumulative_target ¶
cumulative_target(reward, v_next, not_done, gamma)
Standard RL target: r + gamma * V'; terminal -> r.
The ordinary discounted-cumulative-reward backup, so that reward-maximizing
RL is a mode of this library rather than a separate implementation. Unlike
the two safety operators this one carries no margin semantics: the first
argument is a reward, and the resulting V is an expected return whose
sign means nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reward
|
Array
|
immediate reward |
required |
v_next
|
Array
|
bootstrap value |
required |
not_done
|
Array
|
1.0 on non-terminal steps, 0.0 on terminal steps. |
required |
gamma
|
float
|
discount. |
required |
Source code in safety_sb3/backups.py
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | |
target ¶
target(
mode,
g,
v_next,
not_done,
gamma,
l=None,
terminal_type="all",
)
Dispatch to the operator for mode. See the module docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mode
|
str
|
|
required |
g
|
Array
|
the safety margin |
required |
l
|
Optional[Array]
|
required for |
None
|
Source code in safety_sb3/backups.py
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | |
On-policy learners (PPO family)¶
safety_sb3.ppo_base ¶
PPO, written without a fixed Bellman backup — the shared half of the family.
:class:AbstractPPO is everything the PPO-family learners share regardless of
how many players are in the game: the env guards, the rsl_rl-parity training
recipe, the KL-adaptive learning rate, gamma annealing, and the choice of
rollout buffer from _MODE. It deliberately owns no rollout loop — that
is the axis its subclasses split on:
:class:`~safety_sb3.ppo_1p.AbstractPPO1P` — one actor, one buffer.
:class:`~safety_sb3.ppo_2p.AbstractPPO2P` — two actors, two value nets,
two buffers, and a phase machine.
In the PPO family the backup is carried by the rollout buffer, not by
train(): compute_returns_and_advantage substitutes the mode's operator
for the one-step TD target inside GAE. So a learner selects its problem simply
by declaring _MODE, and
:func:safety_sb3.buffers_rollout.rollout_buffer_classes hands it the matching
buffers.
The rsl_rl-parity pieces (validated: they are what lets this learn hard robot
tasks where stock-SB3-PPO-on-g stalls):
-
No timeout bootstrapping (
bootstrap_on_timeout=False, the default). Stock PPO addsgamma * V(terminal)to the reward at truncations (on_policy_algorithm.collect_rollouts). In the two safety modes the reward IS the physical safety marging(s)— an absolute quantity — so adding a value estimate destroys its semantics and silently corrupts the backup the moment episodes start reaching the time limit. rsl_rl's SafetyPPO overridesprocess_env_stepfor exactly this reason; we gate it in the collect loops. -
KL-adaptive learning rate (
adaptive_lr=Truewithdesired_kl). rsl_rl raises the LR when the update KL sits well below target and lowers it when above. SB3'starget_klonly early-stops; it never adjusts the LR, so in the low-KL / weak-gradient regime typical of the sparse safety signal the policy barely moves. Enabling this recovers rsl_rl's behavior.
Observation normalization is orthogonal and handled the SB3 way — wrap the env
in VecNormalize — which mirrors rsl_rl's built-in running obs normalizer.
All knobs are constructor arguments, so downstream projects configure the algorithm without subclassing.
AbstractPPO ¶
Bases: GammaAnnealMixin, PPO
PPO whose Bellman backup is chosen by _MODE (see the module docstring).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bootstrap_on_timeout
|
bool
|
if False (default) skip PPO's timeout value
bootstrapping — correct whenever the reward is a margin |
False
|
normalize_obs
|
bool
|
wrap the env in VecNormalize(norm_obs=True,
norm_reward=False) — obs normalization is needed to match rsl_rl on hard
robot tasks; reward normalization is refused (it corrupts |
False
|
adaptive_lr
|
bool
|
enable rsl_rl-style KL-adaptive learning rate. |
False
|
desired_kl
|
float | None
|
target KL for the adaptive LR controller. |
0.01
|
lr_bounds
|
tuple[float, float]
|
(min, max) bounds for the adaptive LR. |
(1e-05, 0.01)
|
adaptive_lr_factor
|
float
|
multiplicative step for the adaptive LR. |
1.5
|
gamma_anneal
|
discount-factor annealing (ON by default). GPU-resident path: pass a :class: |
True
|
Source code in safety_sb3/ppo_base.py
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | |
collect_rollouts ¶
collect_rollouts(*args, **kwargs)
Refuse to fall through to SB3's OnPolicyAlgorithm.collect_rollouts.
This class has no rollout loop by design — that is the Players axis.
Without this guard, instantiating it directly would inherit the stock
loop, which bootstraps values on timeout (corrupting a margin reward) and
never offers the buffer its per-step extras (so a reach-avoid buffer
would silently back up l = 0 everywhere).
Source code in safety_sb3/ppo_base.py
155 156 157 158 159 160 161 162 163 164 165 166 167 | |
safety_sb3.ppo_1p ¶
Single-player PPO — the 1P half of the PPO family, and its three modes.
AbstractPPO (ppo_base.py — recipe, buffers, adaptive LR)
└─ AbstractPPO1P one actor, one value net, one rollout buffer
├─ SafetyPPO1P _MODE = AVOID
├─ ReachAvoidPPO1P _MODE = REACH_AVOID (+ _ReachAvoidPlumbing)
└─ CumulativePPO1P _MODE = CUMULATIVE
Everything specific to one player lives here: the two rollout loops (numpy /
GPU-resident), which are SB3's OnPolicyAlgorithm.collect_rollouts with the
timeout value-bootstrap gated off and one added call — the buffer is handed each
step's extras so it can keep whatever its own operator needs (see
:mod:safety_sb3.buffers_rollout).
The three concrete learners below are one line each. That is the point of the split: the mode axis costs a line, the players axis costs a loop.
AbstractPPO1P ¶
Bases: AbstractPPO
PPO with the ordinary single-actor rollout, backup chosen by _MODE.
Source code in safety_sb3/ppo_1p.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | |
collect_rollouts ¶
collect_rollouts(
env, callback, rollout_buffer, n_rollout_steps
)
SB3 OnPolicyAlgorithm.collect_rollouts with two safety changes:
the timeout value-bootstrap is gated by self.bootstrap_on_timeout
(off by default — the reward is the physical margin g(s)), and the buffer
is offered each step's infos before add() so it can capture the
extras its operator needs. On the GPU-resident path this dispatches to
_collect_rollouts_tensor.
Source code in safety_sb3/ppo_1p.py
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | |
SafetyPPO1P ¶
Bases: AbstractPPO1P
On-policy avoid RL — Fisac et al. 2019::
V(s) = (1-γ)·g + γ·min(g, V') terminal: V(s) = g
g(s) is the safety margin and rides on the reward channel; V(s) >= 0
iff the state is in the safe set. There is no target set and no l: this
learner never reads info["l_x"] and its buffer has nowhere to put it.
For a task with a target to reach use :class:ReachAvoidPPO1P — the two
operators are not interchangeable (see :mod:safety_sb3.backups).
Source code in safety_sb3/ppo_1p.py
223 224 225 226 227 228 229 230 231 232 233 234 235 | |
ReachAvoidPPO1P ¶
Bases: _ReachAvoidPlumbing, AbstractPPO1P
On-policy reach-avoid RL — Hsu et al. RSS'21 eq. 15::
V(s) = (1-γ)·min(l, g) + γ·min(g, max(l, V'))
g (safety margin) rides on the reward channel; l (target margin) is
read from info["l_x"] on the numpy path and returned directly by
TensorVecEnv.step_tensor on the GPU-resident path — in both cases by the
buffer, not by this class.
Unlike the SAC family this is fully on-policy: it pairs with vectorized envs
at large n_envs (the GPU-parallel regime where off-policy reach-avoid
saturates; validated on a 1280-env MuJoCo quadruped in unitree_rl_mjlab).
Source code in safety_sb3/ppo_1p.py
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | |
CumulativePPO1P ¶
Bases: AbstractPPO1P
Ordinary reward-maximizing PPO — V(s) = r + γ·V(s').
Not a safety learner: the reward channel carries a reward, not a margin, and
V >= 0 means nothing. It is here so a nominal baseline runs through
every line of the same code as the safety learners — the standard control for
"is the safety operator doing the work, or is it just PPO?".
Source code in safety_sb3/ppo_1p.py
256 257 258 259 260 261 262 263 264 265 | |
safety_sb3.ppo_2p ¶
Two-player (adversarial) PPO — the 2P half of the PPO family.
AbstractPPO (ppo_base.py)
└─ AbstractPPO2P two actors, two value nets, two buffers, phases
├─ SafetyPPO2P _MODE = AVOID
└─ ReachAvoidPPO2P _MODE = REACH_AVOID (+ _ReachAvoidPlumbing)
The control player MAXIMIZES the value, the disturbance player MINIMIZES it, and
a league of archived opponents damps cycling. Both players are ordinary PPO
policies over disjoint SUB-spaces of the env's concatenated action space (the
env exposes one Box(ctrl_dim + dstb_dim) and splits it; g rides on the
reward channel, l — reach-avoid only — on info["l_x"]).
- the targets are computed on-policy per rollout, inside the rollout buffer; the min player trains on the SAME targets with a NEGATED advantage (negation commutes with advantage normalization) — the zero-sum property;
- training alternates in phases (dstb pretrain, then K dstb / M ctrl rollout cycles); the frozen player acts stochastically;
- with
n_envs > 1, league opponents are assigned to env SLICES so a single on-policy batch contains rollouts against the whole archived population (validated at 1280 GPU-parallel envs inunitree_rl_mjlab; off-policy two-player SAC saturates at ~32-64 envs).
self.policy is always the CONTROL policy — predict(), save(), and
downstream filter wrappers see the deployable controller.
Why this is NOT the same object as :mod:safety_sb3.sac_2p¶
Both classes hold two actors, and it is tempting to read them as one algorithm with two optimizers. They are not, and the difference is forced by where each family's critic takes its action:
- SAC 2P has ONE twin critic over the JOINT action
Q(s, [a_ctrl, a_dstb]), one replay buffer, and continuous updates. Because the critic takes the action, that single Q is the game value, and both actors differentiate through the same object — ctrl ascends it, dstb descends it. Nothing needs to be scheduled, and replay makes the learner indifferent to whose policy generated the data. This is the minimax game as ISAACS formulates it. - PPO 2P (here) has TWO independent state-only value nets
V(s), two rollout buffers, and a phase machine. A state-only critic cannot represent "value of this joint action", so each player needs its own advantage, its own importance ratio, and — because PPO is on-policy — its own freshly collected data. That is exactly what the phase machine exists for, and exactly why SAC needs no phases. This is an alternating best-response approximation, not the simultaneous saddle point.
They are therefore not interchangeable, and there is deliberately no cross-family
AbstractTwoPlayer base: the two differ in their state — one critic vs two,
one buffer vs two, phases vs none — not merely in their update loops. A shared
parent could only hold the names.
AbstractPPO2P ¶
Bases: AbstractPPO
Two-player on-policy game with league opponents, backup chosen by _MODE.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ctrl_action_dim
|
int
|
number of leading action dims belonging to the control player; the rest are the disturbance. |
-1
|
dstb_pretrain_rollouts
|
int
|
rollouts spent training only the disturbance before the alternation starts. |
20
|
ctrl_rollouts_per_cycle
|
int
|
control-player rollouts per alternation cycle. |
4
|
dstb_rollouts_per_cycle
|
int
|
disturbance-player rollouts per alternation cycle. |
1
|
use_leaderboard
|
bool
|
archive opponents and assign them to env slices. |
False
|
dstb_learning_rate
|
float | None
|
disturbance-player learning rate; |
None
|
dstb_ent_coef
|
float | None
|
disturbance-player entropy coefficient; |
None
|
Source code in safety_sb3/ppo_2p.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 | |
collect_rollouts ¶
collect_rollouts(
env, callback, rollout_buffer, n_rollout_steps
)
Two-player rollout: both actions computed each step, env receives the concatenation, only the ACTIVE player's data is stored (in its buffer).
Source code in safety_sb3/ppo_2p.py
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 | |
SafetyPPO2P ¶
Bases: AbstractPPO2P
Two-player on-policy avoid game — ISAACS proper (Hsu, Nguyen, Fisac 2022, eq. 7), with league opponents::
V(s) = (1-γ)·g + γ·max_ctrl min_dstb min(g, V')
the robust-invariance value under a worst-case disturbance. No target set, no
l — the paper has neither, so info["l_x"] is never read and the league
scores an episode a WIN when the ctrl player survives to the time limit without
ever violating g.
This is the class for an adversarial avoid task ("stay standing against a
worst-case force"). Do NOT emulate it by giving :class:ReachAvoidPPO2P a
degenerate l: no l reduces the reach-avoid operator to avoid
(:mod:safety_sb3.backups proves the two conditions are contradictory).
Source code in safety_sb3/ppo_2p.py
619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 | |
ReachAvoidPPO2P ¶
Bases: _ReachAvoidPlumbing, AbstractPPO2P
Two-player on-policy reach-avoid game — Gameplay Filters (Hsu et al.
2024, eq. 6a), which extends ISAACS to reach-avoid: anchor min(l, g).
Needs a target margin l (info["l_x"] on the numpy path,
step_tensor's l_x on the GPU-resident path). The league scores an
episode a ctrl WIN only if the controller both survived to the time limit and
reached the target. For a two-player avoid game (no target set) use
:class:SafetyPPO2P.
Source code in safety_sb3/ppo_2p.py
639 640 641 642 643 644 645 646 647 648 649 650 | |
Off-policy learners (SAC family)¶
safety_sb3.sac_base ¶
SAC, written without a fixed Bellman backup — the shared half of the family.
:class:AbstractSAC is everything the SAC-family learners share regardless of
how many players are in the game: the constructor and mode check, replay-buffer
selection and validation, the entropy-temperature bounds / reset, gamma
annealing, the GPU-resident collection loop, and the TD target itself. It
deliberately owns no train() — that is the axis its subclasses split on:
:class:`~safety_sb3.sac_1p.AbstractSAC1P` — one actor, one entropy temp.
:class:`~safety_sb3.sac_2p.AbstractSAC2P` — two actors over disjoint action
sub-spaces, two entropy temps, ONE twin critic over the joint action.
Which value function a learner converges to is a parameter, not a class: the
update loops form the soft next-state value V' and hand it to
:func:safety_sb3.backups.target together with self._MODE.
``_MODE = backups.AVOID`` -> SafetySAC1P / SafetySAC2P
``_MODE = backups.REACH_AVOID`` -> ReachAvoidSAC1P / ReachAvoidSAC2P
``_MODE = backups.CUMULATIVE`` -> CumulativeSAC1P (ordinary SAC)
Each of those is a one-line specialization; the mode is also selectable per
instance via the mode= constructor argument. This follows Bertsekas's
abstract-DP framing (define the algorithm against an abstract operator, then
choose the operator) and it is why the reach-avoid learner cannot drift from the
avoid learner again: before this class they were two hand-copied train()
bodies, and the copy had already lost the min_alpha/max_alpha clamp.
Note that SAC needs no reach-avoid mixin, unlike the on-policy family: its whole
mode delta is the buffer choice and the l-carrying flag below, both of which
already dispatch on _MODE right here.
AbstractSAC ¶
Bases: GammaAnnealMixin, SAC
SAC whose Bellman backup is chosen by _MODE (see the module docstring).
Subclasses SB3's SAC to reuse the actor, entropy regularization, replay
buffer, etc. Abstract: it has no train().
GPU-resident path: pass a TensorVecEnv (detected via is_tensor_env,
optionally auto-wrapped in TensorVecNormalize with normalize_obs=True)
and collection + replay stay on device end-to-end
(:class:safety_sb3.tensor_replay.TensorReplayBuffer); train() is
unchanged -- the tensor buffer returns the same named tuples. NOTE on the
tensor path set gradient_steps explicitly (>=1): the SB3 convention
gradient_steps=-1 ("as many as env steps") would mean num_envs updates
per vector step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mode
|
str | None
|
backup to converge to, one of :data: |
None
|
terminal_type
|
str
|
terminal target for the reach-avoid backup; ignored by
the other modes. See :func: |
'all'
|
Source code in safety_sb3/sac_base.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 | |
train ¶
train(gradient_steps, batch_size)
Refuse to fall through to SB3's SAC.train.
This class has no update loop by design — that is the Players axis. Without
this guard, instantiating it directly would inherit stock SAC's train() and
silently converge to the CUMULATIVE fixed point while _MODE claimed
otherwise: exactly the "different fixed points under one name" bug the
library exists to prevent.
Source code in safety_sb3/sac_base.py
375 376 377 378 379 380 381 382 383 384 385 386 387 | |
safety_sb3.sac_1p ¶
Single-player SAC — the 1P half of the SAC family, and its three modes.
AbstractSAC (sac_base.py — ctor, buffers, entropy, backup)
└─ AbstractSAC1P the single-actor update loop
├─ SafetySAC1P _MODE = AVOID
├─ ReachAvoidSAC1P _MODE = REACH_AVOID
└─ CumulativeSAC1P _MODE = CUMULATIVE
There is exactly ONE train() here and the three learners below are one line
each. No reach-avoid mixin is needed on this side of the library: SAC's entire
mode delta is the replay-buffer choice and the TD target, and both already
dispatch on _MODE inside :class:~safety_sb3.sac_base.AbstractSAC.
That de-duplication is load-bearing, not cosmetic. These used to be two
hand-copied train() bodies, and the copy had silently dropped
_clamp_entropy_temps() — so min_alpha/max_alpha did nothing on the
reach-avoid path. One shared body makes that unrepresentable.
AbstractSAC1P ¶
Bases: AbstractSAC
SAC's single-actor update loop; the backup is chosen by _MODE.
Source code in safety_sb3/sac_1p.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | |
train ¶
train(gradient_steps, batch_size)
Largely follows the original SAC train method from stable_baselines3.
The TD target is the one for self._MODE (:meth:_bellman_target), so
this one body serves every single-player mode in the library.
Source code in safety_sb3/sac_1p.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | |
SafetySAC1P ¶
Bases: AbstractSAC1P
SAC with the avoid (safety) Bellman backup — Fisac et al. 2019::
V(s) = (1-γ)·g + γ·min(g, V') terminal: V(s) = g
g(s) is the safety margin (it rides on the reward field); V' is
SAC's soft next-state value. V(s) >= 0 iff the state is in the safe set.
For a task with a target to reach use :class:ReachAvoidSAC1P; the two
operators are not interchangeable (see :mod:safety_sb3.backups).
Source code in safety_sb3/sac_1p.py
187 188 189 190 191 192 193 194 195 196 197 198 199 | |
ReachAvoidSAC1P ¶
Bases: AbstractSAC1P
SAC with the reach-avoid Bellman backup — Hsu et al. RSS'21::
non-terminal: y = (1-γ)·min(l, g) + γ·min(g, max(l, V'))
terminal: y = min(l, g)
g(s) is the safety/avoid margin (rides on the reward field) and
l(s) is the target/reach margin, supplied by the env via info["l_x"]
and stored by
:class:~safety_sb3.buffers_replay.ReachAvoidReplayBuffer — which the base
defaults to and validates off this _MODE. Matches the discounted
reach-avoid update in safe_adaptation_dev
(utils/train.get_bellman_update, mode='reach-avoid',
terminal_type='all').
Tensor path (TensorVecEnv): the device-resident
:class:~safety_sb3.tensor_replay.TensorReplayBuffer is built with
store_l=True (l comes from step_tensor's l_x, not infos).
Source code in safety_sb3/sac_1p.py
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | |
CumulativeSAC1P ¶
Bases: AbstractSAC1P
Ordinary reward-maximizing SAC — y = r + γ·V'.
Not a safety learner: the reward field carries a reward, not a margin, and
V >= 0 means nothing. It exists so a nominal baseline runs through every
line of the same actor/critic/entropy code as the safety learners — the
standard control for "is the safety operator doing the work, or is it just
SAC?".
Source code in safety_sb3/sac_1p.py
226 227 228 229 230 231 232 233 234 235 236 237 | |
safety_sb3.sac_2p ¶
Two-player (adversarial) SAC — the 2P half of the SAC family.
AbstractSAC (sac_base.py)
└─ AbstractSAC2P two actors, two entropy temps, ONE joint critic
├─ SafetySAC2P _MODE = AVOID (ISAACS proper)
└─ ReachAvoidSAC2P _MODE = REACH_AVOID (Gameplay Filters)
Control actor (max-player) and disturbance actor (min-player) share one twin
critic over the full concatenated action Q(s, [a_ctrl, a_dstb]). Each actor
has its own entropy coefficient / target entropy (ctrl: -ctrl_dim, dstb:
-dstb_dim). Uses :class:~safety_sb3.policies.TwoPlayerSACPolicy (two
sub-space actors + one full critic).
PURE Hamilton-Jacobi value in the critic target — NO entropy::
V' = min(Q1', Q2') (no soft term)
y = backups.target(mode, g, V', ...) (avoid | reach-avoid)
The per-actor temperatures α_ctrl, α_dstb appear ONLY in the ctrl/dstb actor
losses (exploration; annealed away over training), never in the critic target.
This is ISAACS eq. 8a exactly (Hsu et al. 2023, entropy_motives=0), and it is
what both reference codebases do (base_block.py adds entropy to the target only
if self.mode == 'performance'). A soft term in the target would tax the game
value whose zero level set is the online safety certificate, warping {V>=0}.
(An earlier version of this file put - α_ctrl·logπ_ctrl + α_dstb·logπ_dstb in
V' and mislabelled it "as ISAACS formulates it"; that was a port error, now
corrected — see the note at the target computation.)
This is the minimax game as ISAACS formulates it, and it is NOT the same
object as :mod:safety_sb3.ppo_2p even though both hold two actors. Because
SAC's critic takes the action, this one Q is the game value and both actors
differentiate through it; nothing needs scheduling, and replay makes the learner
indifferent to whose policy produced the data. The on-policy two-player learner
has two state-only V(s) nets, two rollout buffers and a phase machine, and is
an alternating best-response approximation. See :mod:safety_sb3.ppo_2p for the
full comparison and for why there is deliberately no shared
AbstractTwoPlayer base.
AbstractSAC2P ¶
Bases: AbstractSAC
Two-player off-policy game; the backup is chosen by _MODE.
Source code in safety_sb3/sac_2p.py
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 | |
SafetySAC2P ¶
Bases: AbstractSAC2P
Two-player off-policy avoid game — ISAACS proper (Hsu et al. 2022, eq. 7)::
V(s) = (1-γ)·g + γ·max_ctrl min_dstb min(g, V')
the robust-invariance value under a worst-case disturbance. No target set, no
l — the paper has neither.
This is the class for an adversarial avoid task ("stay standing against a
worst-case force"). Do NOT emulate it by giving :class:ReachAvoidSAC2P a
degenerate l: no l reduces the reach-avoid operator to avoid
(:mod:safety_sb3.backups proves the two conditions are contradictory).
l is ignored if present, so an l-carrying replay buffer is harmless.
Source code in safety_sb3/sac_2p.py
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 | |
ReachAvoidSAC2P ¶
Bases: AbstractSAC2P
Two-player off-policy reach-avoid game — Gameplay Filters (Hsu et al.
2024, eq. 6a), which extends ISAACS to reach-avoid: anchor min(l, g).
Needs a target margin l; the base defaults the replay buffer to the
l-carrying one off this _MODE. For a two-player avoid game (no
target set) use :class:SafetySAC2P.
Source code in safety_sb3/sac_2p.py
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 | |
A2C and DQN¶
safety_sb3.a2c ¶
A2C, written without a fixed Bellman backup.
AbstractA2C
├─ SafetyA2C1P _MODE = AVOID
├─ ReachAvoidA2C1P _MODE = REACH_AVOID (+ _ReachAvoidPlumbing)
└─ CumulativeA2C1P _MODE = CUMULATIVE
A2C is single-player only — there is no AbstractA2C2P. Nothing in the
two-player construction is A2C-specific, but nobody has needed it, and the
library does not ship learners it has not exercised.
Like PPO, A2C carries its backup in the rollout buffer, so a learner picks
its problem by declaring _MODE and the matching buffer pair follows. Unlike
PPO, A2C does not own its rollout loop: it uses SB3's stock
OnPolicyAlgorithm.collect_rollouts unchanged, so there is no place to gate
the timeout value-bootstrap and no place to hand the buffer its per-step extras.
The extras still get through, because SB3's loop calls _update_info_buffer
once per step between env.step() and rollout_buffer.add() — exactly the
point the reach-avoid buffer needs — and :meth:AbstractA2C._update_info_buffer
forwards from there. That is the whole cost of reach-avoid A2C.
.. warning::
A2C here does NOT gate PPO's timeout value-bootstrap, because it does not
override the collect loop. When episodes hit the time limit, SB3 adds
gamma * V(terminal) to the reward — and in the two safety modes the
reward is the physical margin g(s), so that corrupts the backup. Prefer
the PPO-family learners for safety work; A2C is kept for parity and for
cheap baselines.
AbstractA2C ¶
Bases: GammaAnnealMixin, A2C
A2C whose Bellman backup is chosen by _MODE.
gamma_anneal (ON by default) anneals the discount 0.99 -> 0.9999 over the
first 50% of training (read off rollout_buffer.gamma in GAE); applied via
_update_current_progress_remaining each iteration. See gamma_anneal.py.
Source code in safety_sb3/a2c.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
SafetyA2C1P ¶
Bases: AbstractA2C
A2C with the avoid backup — V(s) = (1-γ)·g + γ·min(g, V').
Source code in safety_sb3/a2c.py
86 87 88 89 | |
ReachAvoidA2C1P ¶
Bases: _ReachAvoidPlumbing, AbstractA2C
A2C with the reach-avoid backup —
V(s) = (1-γ)·min(l, g) + γ·min(g, max(l, V')).
l arrives via info["l_x"] and is captured by the buffer (see the
module docstring for how it reaches there without an A2C rollout loop).
Source code in safety_sb3/a2c.py
92 93 94 95 96 97 98 99 100 | |
CumulativeA2C1P ¶
Bases: AbstractA2C
Ordinary reward-maximizing A2C — V(s) = r + γ·V(s'), i.e. stock A2C
reached through this library's mode dispatch. Not a safety learner.
Source code in safety_sb3/a2c.py
103 104 105 106 107 | |
safety_sb3.dqn ¶
DQN, written without a fixed Bellman backup.
AbstractDQN
├─ SafetyDQN1P _MODE = AVOID
└─ CumulativeDQN1P _MODE = CUMULATIVE
There is deliberately no ReachAvoidDQN1P. MAP would predict one, and it is
the single place in the taxonomy where the product does not close: SB3's
discrete-action ReplayBuffer carries no target margin l(s), and the
reach-avoid operator cannot be evaluated without it. That is a missing
capability, not a naming gap, so :class:AbstractDQN refuses
mode='reach-avoid' loudly at construction rather than silently computing
something else. Use :class:~safety_sb3.sac_1p.ReachAvoidSAC1P (continuous
actions) or add an l-carrying discrete replay buffer.
DQN is also single-player only — a two-player discrete game would need a joint action-value table over both players' actions, which is a different learner.
AbstractDQN ¶
Bases: GammaAnnealMixin, DQN
DQN whose TD target is the backup for self._MODE, dispatched through
:mod:safety_sb3.backups like every other learner in the library.
gamma_anneal (ON by default) anneals the discount 0.99 -> 0.9999 over the
first 50% of training (read as self.gamma in the TD target of
train()); the numpy off-policy loop applies it via
_update_current_progress_remaining. See gamma_anneal.py.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mode
|
str | None
|
backup to converge to; defaults to the class's |
None
|
Source code in safety_sb3/dqn.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
train ¶
train(gradient_steps, batch_size)
Largely follows the original DQN train method from stable_baselines3.
The TD target is the backup for self._MODE (safety_sb3.backups).
Source code in safety_sb3/dqn.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
SafetyDQN1P ¶
Bases: AbstractDQN
DQN with the avoid backup — Q(s,a) = (1-γ)·g + γ·min(g, max_a' Q')
(Fisac et al. 2019). g(s) rides on the reward channel.
Source code in safety_sb3/dqn.py
118 119 120 121 122 | |
CumulativeDQN1P ¶
Bases: AbstractDQN
Ordinary DQN — Q(s,a) = r + γ·max_a' Q'. Not a safety learner; it is
here so a reward-maximizing discrete baseline shares this library's code.
Source code in safety_sb3/dqn.py
125 126 127 128 129 | |
The reach-avoid mixin¶
safety_sb3.reach_avoid_mixin ¶
The reach-avoid plumbing for the on-policy family — a MIXIN, not a base.
Why a mixin. MAP is a product: Mode x Algorithm x Players. Single inheritance can linearise only one axis, so one of them has to be composition, and the players axis is the one that carries the code — the two on-policy update loops are hundreds of lines apart, while the reach-avoid delta below is a couple of dozen. Making reach-avoid a parent of the loops would therefore mean duplicating a loop per mode; making it a mixin means it composes with both::
class ReachAvoidPPO1P(_ReachAvoidPlumbing, AbstractPPO1P): _MODE = REACH_AVOID
class ReachAvoidPPO2P(_ReachAvoidPlumbing, AbstractPPO2P): _MODE = REACH_AVOID
class SafetyPPO1P(AbstractPPO1P): _MODE = AVOID
and — the part that matters for correctness — the avoid learners never receive
it at all. Before v0.4.0 the avoid classes inherited the reach-avoid class and
switched its machinery back off through an _is_reach_avoid predicate; every
piece of l-handling then had to be guarded, and one missed guard silently
fed an avoid learner a target margin. There is nothing to switch off now.
What is left here is deliberately small. The capture of l(s) itself used to
live in the algorithm (a _record_step_extras hook on the rollout loop) and
was the bulk of this file; it now lives in the rollout buffers, which is where
the rest of the l plumbing already was — see
:mod:safety_sb3.buffers_rollout. What remains is one constructor argument and
the reach half of the two-player league's win condition.
SAC needs no equivalent: its mode delta is a handful of lines that already sit
inside :class:~safety_sb3.sac_base.AbstractSAC and dispatch on _MODE.
The two-player league¶
safety_sb3.leaderboard ¶
The league: a policy archive shared by both two-player families.
Mirrors safe_adaptation_dev's leaderboard: a checkpoint archive of the best
control (max-player) and disturbance (min-player) actors, scored against the
opponents on the board, with worst-performer eviction and softmax-rationality
sampling of past disturbances into rollouts (the "strategy-space
regularization" that damps cycling).
Score matrix board has shape (kc+1, kd+2):
* rows 0..kc-1 = saved ctrl checkpoints; row -1 = the current ctrl.
* cols 0..kd-1 = saved dstb checkpoints; col -2 = the current dstb;
col -1 = the dummy (no-disturbance) opponent.
board[i, j] = success metric of ctrl i vs dstb j (higher = ctrl
better / dstb weaker). Control is a maximizer; disturbance a minimizer.
Both two-player families use :class:Leaderboard, but they FILL it differently
and that difference is deliberate, not an oversight:
- the off-policy (SAC) league scores with dedicated evaluation episodes on a
separate eval env (:class:
LeagueEvaluatorbelow), writes cells with :meth:Leaderboard.set_score(an overwrite), and samples ONE archived opponent per rollout via :meth:Leaderboard.sample_dstb_step; - the on-policy (PPO) league has no eval env at all. It scores from the
training rollout outcomes it already produced, which are noisy, so it
accumulates them with :meth:
Leaderboard.ema_score, and it faces a whole POPULATION of opponents at once by assigning them to env slices via :meth:Leaderboard.sample_dstb_slices.
Neither is a special case of the other: dedicated-eval + overwrite + one opponent is not the same estimator as training-outcome + EMA + population, and on-policy learning cannot reuse another opponent's data the way replay can. So this module offers both vocabularies rather than forcing one.
Leaderboard ¶
Source code in safety_sb3/leaderboard.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | |
set_score ¶
set_score(ctrl_idx, dstb_idx, metric)
Overwrite a cell — used when the score came from a dedicated eval run
(contrast :meth:ema_score).
Source code in safety_sb3/leaderboard.py
79 80 81 82 | |
prune ¶
prune(step, ctrl_actor, dstb_actor)
Admit the current ctrl/dstb (row -1 / col -2) into the archive, evicting
the worst performer when full. Matches safe_adaptation_dev.prune_leaderboard.
Source code in safety_sb3/leaderboard.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
sample_dstb_step ¶
sample_dstb_step()
Return a saved dstb step to roll out against, or None for the current
dstb / dummy. Probability ∝ exp(-rationality · avg_metric): a disturbance
that drives the control's reach-avoid value down (low metric) is sampled
more often. Falls back to the current dstb when the board is empty.
Source code in safety_sb3/leaderboard.py
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
ema_score ¶
ema_score(ctrl_idx, dstb_idx, metric, beta=0.1)
EMA score update — the on-policy board is filled from noisy per-rollout
TRAINING outcomes rather than from dedicated eval runs, so cells are
accumulated instead of overwritten (contrast :meth:set_score).
Source code in safety_sb3/leaderboard.py
137 138 139 140 141 142 143 144 145 146 | |
sample_dstb_slices ¶
sample_dstb_slices(n_slices)
Opponent assignment for one on-policy rollout over env SLICES.
Returns a list of length n_slices with entries: -3 = ZERO (dummy
opponent; also anchors nominal behavior), -2 = RANDOM, -1 =
CURRENT dstb, >= 0 = index into dstb_steps (archived checkpoint).
Slices 0 and 1 are always ZERO and RANDOM; the rest are softmax-sampled
with p ∝ exp(-rationality * avg_metric) over {archived, CURRENT}.
Source code in safety_sb3/leaderboard.py
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | |
LeagueEvaluator ¶
Scores one (ctrl, dstb) pair by rolling out dedicated eval episodes.
This is the off-policy league's estimator, lifted out of the algorithm so that everything about the league lives in this module. It picks the cheapest of three equivalent rollout paths automatically:
step_tensoron a GPU-resident env — everything on device, no numpyVecEnvand no per-step host<->device sync. ~10-50x faster than the numpy path and the main league-throughput fix (profiling: the numpy vectorized path cost ~100s per refresh);- a parallel numpy
VecEnv— one batch =num_envsfirst-episodes; - a single raw
gym.Envloop.
All three share the success semantics, which the caller supplies:
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
success
|
|
required | |
obs_normalizer
|
zero-arg callable returning the LIVE training
normalizer (or |
None
|
Source code in safety_sb3/leaderboard.py
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | |
score ¶
score(ctrl_actor, dstb_actor)
Success rate of ctrl vs dstb (None = dummy / no disturbance).
Source code in safety_sb3/leaderboard.py
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | |
Policies¶
safety_sb3.policies ¶
Policies. Currently one: the two-player SAC policy.
:class:TwoPlayerSACPolicy is SAC-only, and the name says so deliberately.
It exists because the SAC-family two-player game needs ONE critic over the JOINT
action Q(s, [a_ctrl, a_dstb]) — so both actors and that shared critic have to
be built together, inside a single policy object. The PPO-family two-player
learner has no use for it: PPO's critic is state-only (V(s)), so each player
gets an independent, ordinary single-player policy built over its own action
sub-space, and :class:~safety_sb3.ppo_2p.AbstractPPO2P simply holds two of
them. Naming this a bare TwoPlayerPolicy would overclaim.
It subclasses SB3's :class:SACPolicy so the two-player learners stay
plug-and-play for existing SB3 code: model.predict / save / load behave
normally and return the control action. Internally it holds two actors over
disjoint sub-action spaces and one twin critic over the FULL (concatenated)
action:
self.actor— control actor (max-player), over the firstctrl_action_dimaction dims. This is the deployable safety controller;predictuses it.self.dstb_actor— disturbance actor (min-player), over the remaining dims.self.critic/self.critic_target—Q(s, [a_ctrl, a_dstb])over the full action space.
The env's action_space is the concatenation Box(ctrl_dim + dstb_dim); the
env splits it and applies ctrl as control + dstb as a bounded disturbance.
ctrl_action_dim tells the policy where to split (pass via policy_kwargs).
TwoPlayerSACPolicy ¶
Bases: SACPolicy
Source code in safety_sb3/policies.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | |
predict ¶
predict(
observation,
state=None,
episode_start=None,
deterministic=False,
)
Deployment: return the CONTROL action only (the safety controller, no adversary), in the control sub-space bounds. Overrides SB3's predict, which would reshape the ctrl-dim output to the full concatenated action space.
Source code in safety_sb3/policies.py
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | |
Discount (gamma) annealing¶
safety_sb3.gamma_anneal ¶
Discount-factor (gamma) annealing for the Safety Bellman backups.
WHY THIS EXISTS¶
The reach-avoid / avoid Safety Bellman operator
V = (1 - gamma) * min(l, g) + gamma * min(g, max(l, V'))
is a contraction with a sharp fixed point only in the limit gamma -> 1.
At a fixed gamma = 0.99 the (1 - gamma) anchor keeps bleeding the
immediate margin into the value everywhere, so the learned zero-level set (the
safe/unsafe boundary we filter on) is smeared and the reach term never becomes
crisp. Training the operator directly at gamma -> 1 is ill-conditioned
(the contraction modulus -> 1, bootstrapping dominates, the critic diverges
early). The reach-avoid literature (Fisac 2019; Hsu 2021; the ISAACS
safe_adaptation_dev codebase) fixes this with DISCOUNT ANNEALING: start
contractive at gamma = 0.99 for a well-conditioned early fixed point, then
anneal gamma -> 1 so the boundary sharpens as the critic stabilises.
TWO SCHEDULERS (both provided; StepGammaAnneal is the DEFAULT)¶
-
:class:
StepGammaAnneal— REFERENCE-FAITHFUL discrete jumps (StepLRMargininsafe_adaptation_dev/agent/scheduler.py): the gap(goal - gamma)is multiplied byratioat fixed training-fraction milestones. Default 0.99 -> 0.999 (at 20%) -> 0.9999 (at 40%), then hold. This isis_stepwiseso each jump triggers an ENTROPY-TEMPERATURE (alpha) RESET in the SAC learners (the Q-scale shifts discontinuously at a jump, so the tuned alpha is stale — the reference resets both actors' alpha on every gamma jump; seeGammaAnnealMixin._on_gamma_jump/ the SAC family). -
:class:
GeometricGammaAnneal— a smooth (continuous) log-space interpolation reachingendatanneal_fracthen holding. No discontinuity, so no alpha reset. Available viagamma_anneal=GeometricGammaAnneal(...).
ON by default in every learner; gamma_anneal=False disables it, a
callable frac -> gamma (optionally exposing .is_stepwise) is a custom
schedule. All knobs (start/end/ratio/period/anneal fraction) are constructor
arguments. Gamma is logged as train/gamma (+ train/gamma_jump on a step).
StepGammaAnneal ¶
Discrete-jump gamma schedule (reference StepLRMargin).
At training fraction f, the number of elapsed jumps is
n = floor(f / period_frac) and
gamma(f) = goal - (goal - init) * ratio**n (clamped to <= end)
Defaults (init 0.99, ratio 0.1, period_frac 0.20, end 0.9999) reproduce the
reference progression the user specified: 0.99 -> 0.999 @20% -> 0.9999 @40%,
then hold. is_stepwise = True so the SAC learners reset alpha on each jump.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
init
|
float
|
starting gamma (default 0.99). |
0.99
|
end
|
float
|
final gamma held after the last jump (default 0.9999). |
0.9999
|
ratio
|
float
|
multiplicative decay of the gap |
0.1
|
period_frac
|
float
|
training fraction between jumps (default 0.20; the user's "a jump every 10-20% of allotted time"). |
0.2
|
goal
|
float
|
the limit the gap closes toward — 1.0 for HJ reachability. |
1.0
|
Source code in safety_sb3/gamma_anneal.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
GeometricGammaAnneal ¶
Smooth (continuous) gamma schedule — log-space interpolation of the gap.
gamma(0) = init; gamma(anneal_frac) = end; constant end after.
Equal fractions of progress multiply (goal - gamma) by a constant factor.
is_stepwise = False -> no alpha reset (the change is continuous).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
init
|
float
|
gamma at fraction 0 (default 0.99). |
0.99
|
end
|
float
|
gamma reached at |
0.9999
|
goal
|
float
|
the limit the gap closes toward, |
1.0
|
anneal_frac
|
float
|
training fraction at which |
0.5
|
Source code in safety_sb3/gamma_anneal.py
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
GammaAnnealMixin ¶
Mix-in that anneals self.gamma over training for every learner.
Wiring:
_update_current_progress_remainingre-applies the schedule each iteration -- covers every NUMPY / on-policy path (SB3 calls it in both learn loops).- The GPU-resident collect loops bypass that call, so the PPO and SAC
families (1P and 2P alike) also call
_apply_gamma_anneal()at the top of theircollect_rollouts._applyis idempotent.
Gamma is applied at consumption time: rollout_buffer.gamma for PPO GAE,
self.gamma for the SAC/DQN TD target. On a DISCRETE jump (StepGammaAnneal)
_on_gamma_jump fires -- a no-op here (PPO), overridden by the SAC learners
to reset the entropy temperature(s), mirroring the reference.
Source code in safety_sb3/gamma_anneal.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
make_default_gamma_schedule ¶
make_default_gamma_schedule(init=0.99)
The default on-by-default schedule: REFERENCE-FAITHFUL discrete jumps
(init -> 0.999 @20% -> 0.9999 @40%, hold), alpha reset on each jump.
Source code in safety_sb3/gamma_anneal.py
144 145 146 147 | |
Environments and normalization¶
The GPU-resident tensor path. Subclass TensorVecEnv and implement step_tensor; wrap
in TensorVecNormalize for on-device observation normalization. See
the environment contract.
safety_sb3.tensor_env ¶
GPU-resident vectorized-env contract for massive-parallel safety training.
Standard SB3 VecEnv is numpy end-to-end: with thousands of GPU-simulated
envs (mjlab / Isaac) every step pays a device->host->device bounce for obs,
rewards and infos, and the rollout buffer re-uploads at train time. The tensor
path keeps EVERYTHING on device:
env.step_tensor(actions) -> (obs, reward_g, dones, timeouts, l_x)
all torch.Tensor on env.device. The learners detect
is_tensor_env and switch to a torch-native collect_rollouts +
:mod:safety_sb3.tensor_buffers (identical backup math, no numpy anywhere on
the hot path).
Reward semantics are unchanged: the reward IS the safety margin g(s);
l_x is the target margin for reach-avoid (zeros for avoid-only envs).
Reward normalization does not exist on this path by design (it would corrupt
the Safety Bellman backup); observation normalization is provided by
:class:TensorVecNormalize (an rsl_rl-style running normalizer on device).
Subclass :class:TensorVecEnv for your simulator bridge; it also subclasses
SB3's VecEnv so BaseAlgorithm accepts it (the numpy step API raises).
TensorVecEnv ¶
Bases: VecEnv
Torch-native VecEnv. Implement reset and step_tensor.
Source code in safety_sb3/tensor_env.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
step_tensor ¶
step_tensor(actions)
-> (obs, reward_g, dones, timeouts, l_x); all on self.device.
dones = terminated | truncated (env auto-resets internally);
timeouts = truncated & ~terminated; l_x = target margin (zeros
for avoid-only envs).
Source code in safety_sb3/tensor_env.py
50 51 52 53 54 55 56 57 58 | |
metrics ¶
metrics()
Optional per-rollout scalars (curriculum levels, task metrics) that the algorithm forwards into its logger. Never train on these.
Source code in safety_sb3/tensor_env.py
60 61 62 63 | |
TensorVecNormalize ¶
Bases: TensorVecEnv
Running observation normalizer on device (rsl_rl-parity).
Wraps a :class:TensorVecEnv; normalizes observations with running
mean/var updated during training (freeze with training=False). There is
deliberately NO reward normalization on this path — the reward is the
physical margin g(s).
Source code in safety_sb3/tensor_env.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
executed_action
property
¶
executed_action
What the WRAPPED env actually applied last step, or None.
An env may execute something other than the action it was handed -- a
training-time safety filter substituting its fallback is the case this
exists for -- and the off-policy collector reads it back so the replay
buffer holds the transition that really happened. This class has no
__getattr__, so without an explicit forward the readback would find
nothing on the wrapper, silently keep the PROPOSED action, and the buffer
would fill with actions that were never applied. That failure is invisible
from the outside: training runs, the curves look plausible, and the critic
is fit to transitions that never occurred.
normalize_obs_np ¶
normalize_obs_np(obs)
Numpy convenience for eval/video harnesses.
Source code in safety_sb3/tensor_env.py
143 144 145 146 | |
Callbacks¶
safety_sb3.eval_callbacks ¶
Periodic SAFE-RATE / SUCCESS-RATE evaluation for the tensor training stack.
A standalone SB3 :class:~stable_baselines3.common.callbacks.BaseCallback that,
every eval_freq env-steps, rolls out the current policy on a GPU-resident
eval env and records three scalars to the SB3 logger (which syncs to wandb):
eval/safe_rate— fraction of episodes that NEVER entered the failure set (never sawg < 0).eval/success_rate— for reach-avoid tasks: fraction that reached the target (everl_x >= 0) AND stayed safe; for avoid-only tasks it equalssafe_rate(the reach term does not apply).eval/ep_len_mean— mean length of the tracked first episodes.
The definitions mirror the reference audit target safe_adaptation_dev
(utils/eval.py evaluate_zero_sum: safe_rate = mean(results != -1),
ra_rate = mean(results == 1); result codes success=1 / failure=-1 /
timeout=0) and the league eval in :mod:safety_sb3.leaderboard
(:class:~safety_sb3.leaderboard.LeagueEvaluator: safe = never g < 0,
reached = ever l_x >= 0).
Rollout accounting is parallel: the eval env runs num_envs episodes at once,
we track each env's FIRST episode (safe until its first g < 0, reached if it
ever hit l_x >= 0 before that env's first done), and accumulate
ceil(n_rollouts / num_envs) batches so at least n_rollouts first-episodes
are scored. Everything stays on device — the eval env speaks the tensor API
obs, g, dones, timeouts, l_x = env.step_tensor(actions).
SafeSuccessRateEvalCallback ¶
Bases: BaseCallback
Periodic safe-rate / success-rate eval on a :class:TensorVecEnv.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
eval_env
|
a GPU-resident tensor env ( |
required | |
n_rollouts
|
int
|
minimum number of first-episodes to score per eval. The
callback runs |
100
|
eval_freq
|
int
|
run an eval every |
1000000
|
reach_avoid
|
bool
|
|
True
|
max_ep_steps
|
int
|
hard cap on rollout length (guards against episodes that never terminate). A capped, never-violated episode counts as safe. |
400
|
deterministic
|
bool
|
use deterministic policy actions during eval. |
True
|
Source code in safety_sb3/eval_callbacks.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | |
safety_sb3.callbacks ¶
Callbacks for stable safety-policy fine-tuning.
Empirically, on-policy training against margin-only objectives (safety or
reach-avoid backups) carries no gait-/action-quality gradient, so the usual
PPO entropy dynamics can inflate the action std until a converged motor skill
erodes. Capping the std is a one-line remedy that preserved fine-tuned
capability where uncapped runs lost it. Pair with SB3's native target_kl
early-stop for the actor epochs.
StdCapCallback ¶
Bases: BaseCallback
Clamp the policy's action std at the start of every rollout.
Works with any SB3 on-policy algorithm whose policy exposes a state-
independent log_std parameter (the default MlpPolicy for Box
action spaces). Policies without log_std are silently ignored.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_std
|
float
|
hard upper bound on the (post-clamp) action std. |
required |
Source code in safety_sb3/callbacks.py
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | |
Buffers¶
safety_sb3.buffers_rollout ¶
On-policy rollout buffers — one per Mode, player-agnostic.
Rollout buffers carry the M of MAP and nothing else. A buffer does not know
or care whether one player or two produced the data in it, so there is no
1P/2P in these names: :class:~safety_sb3.ppo_2p.AbstractPPO2P builds
two instances of the very same class, one per player.
Each buffer computes the backup for its _MODE through
:func:safety_sb3.backups.target, so the operator is a parameter rather than a
class body; mode= overrides the class default per instance.
Each buffer also captures its own extras. The safety margin g(s) always
rides on the reward channel, but the reach-avoid operator additionally needs the
target margin l(s), which SB3 delivers only in infos. Rather than teach
every algorithm to thread l, the collect loop hands each step's infos to
the buffer via :meth:record_extras and the buffer keeps whatever its own
operator needs — nothing, for the two modes that have no l. That is why the
reach-avoid learners need almost no reach-avoid code.
SafetyRolloutBuffer ¶
Bases: RolloutBuffer
Rollout buffer for the avoid backup (Fisac et al. 2019).
g(s) rides on the reward channel; the operator is
V(s) = (1-gamma)*g + gamma*min(g, V(s')) with terminal target g.
See :mod:safety_sb3.backups, which defines it.
Source code in safety_sb3/buffers_rollout.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
record_extras ¶
record_extras(infos)
Capture per-step extras from infos into the slot add() will fill.
Called by the on-policy collect loop immediately before add().
No-op here: the avoid operator needs only g, which arrives on the
reward channel. :class:ReachAvoidRolloutBuffer overrides it to keep
l(s).
Source code in safety_sb3/buffers_rollout.py
41 42 43 44 45 46 47 48 | |
compute_returns_and_advantage ¶
compute_returns_and_advantage(last_values, dones)
SB3's GAE loop with this buffer's Bellman backup substituted for the
one-step TD target inside delta (see :meth:_target).
Source code in safety_sb3/buffers_rollout.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
ReachAvoidRolloutBuffer ¶
Bases: SafetyRolloutBuffer
Rollout buffer for the reach-avoid backup (Hsu et al. RSS'21 eq. 15 / Gameplay Filters eq. 6a)::
V(s) = (1-gamma)*min(l, g) + gamma*min(g, max(l, V(s')))
Stores the per-step target margin l(s) beside the safety margin g(s)
(which rides on rewards). l is read out of info["l_x"] by
:meth:record_extras — the buffer captures it itself, which is what keeps
the reach-avoid learners free of l-plumbing.
See :mod:safety_sb3.backups for why the anchor is min(l, g) and not
g, and why avoid is not expressible by degenerating l.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
terminal_type
|
str
|
|
'all'
|
Source code in safety_sb3/buffers_rollout.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | |
record_extras ¶
record_extras(infos)
Keep this step's target margin l(s), read from info["l_x"].
Source code in safety_sb3/buffers_rollout.py
109 110 111 112 | |
CumulativeRolloutBuffer ¶
Bases: SafetyRolloutBuffer
Rollout buffer for the cumulative (ordinary RL) backup
V(s) = r + gamma*V(s') — which makes the loop above exactly stock SB3 GAE.
Not a safety buffer: the reward channel carries a reward, not a margin, and
the resulting value has no zero-level-set meaning. It exists so a
reward-maximizing baseline runs through every line of the same code as the
safety learners (see :mod:safety_sb3.backups).
Source code in safety_sb3/buffers_rollout.py
115 116 117 118 119 120 121 122 123 124 125 | |
rollout_buffer_classes ¶
rollout_buffer_classes(mode)
(numpy_cls, tensor_cls) implementing mode's backup.
The on-policy learners look their buffers up here from their own _MODE
instead of naming them as class attributes, so a buffer cannot drift out of
step with the mode its owner declares. (It used to: the two-player avoid
learner hardcoded the reach-avoid buffer for its min player and so trained
that player on the wrong game.) It also makes every concrete learner in the
library a genuine one-liner — _MODE = ... and nothing else.
Source code in safety_sb3/buffers_rollout.py
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
safety_sb3.buffers_tensor ¶
GPU-resident rollout buffers — torch twins of :mod:safety_sb3.buffers_rollout.
Identical math, no numpy on the hot path. One class per Mode, player-agnostic
(see the numpy module for why buffers carry no 1P/2P). Both families
call the same operators from :mod:safety_sb3.backups; tests/test_backups.py
asserts exact parity.
get() yields standard RolloutBufferSamples whose fields are device
tensors, so stock PPO.train() consumes them unchanged. values /
returns are exposed as numpy properties (PPO's explained-variance logging
touches them once per update).
:meth:TensorSafetyRolloutBuffer.record_extras is the tensor twin of the numpy
buffers' infos capture: on this path the env returns l_x directly from
step_tensor, so the buffer receives the tensor rather than a list of dicts.
TensorSafetyRolloutBuffer ¶
Torch rollout buffer with the AVOID (safety) Bellman backup.
Source code in safety_sb3/buffers_tensor.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | |
record_extras ¶
record_extras(l_x)
Capture per-step extras into the slot add() will fill.
Tensor twin of the numpy buffers' record_extras(infos): the GPU-resident
env hands back l_x from step_tensor directly, so there are no infos
to parse. No-op here — the avoid operator needs only g.
Source code in safety_sb3/buffers_tensor.py
85 86 87 88 89 90 91 | |
TensorReachAvoidRolloutBuffer ¶
Bases: TensorSafetyRolloutBuffer
Torch rollout buffer with the reach-avoid backup (adds l_x).
Torch twin of :class:safety_sb3.buffers_rollout.ReachAvoidRolloutBuffer; see
it for the operator, the anchor rationale, and terminal_type.
Source code in safety_sb3/buffers_tensor.py
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | |
record_extras ¶
record_extras(l_x)
Keep this step's target margin l(s) (straight off step_tensor).
Source code in safety_sb3/buffers_tensor.py
163 164 165 | |
TensorCumulativeRolloutBuffer ¶
Bases: TensorSafetyRolloutBuffer
Torch twin of :class:safety_sb3.buffers_rollout.CumulativeRolloutBuffer —
the ordinary discounted-return backup, i.e. stock GAE on device.
Source code in safety_sb3/buffers_tensor.py
168 169 170 171 172 | |
safety_sb3.buffers_replay ¶
Off-policy replay buffer for the reach-avoid Mode — player-agnostic.
Like the rollout buffers, replay buffers carry the M of MAP and nothing
else: both the single-player and two-player reach-avoid SAC learners store into
the same class (the two-player game concatenates [a_ctrl, a_dstb] into the
ordinary action field, so no extra column is needed).
ReachAvoidReplayBuffer extends SB3's :class:ReplayBuffer with the target
margin l(s) that the reach-avoid backup needs::
V(s) = (1-gamma)*min(l, g) + gamma*min(g, max(l, V(s')))
The safety margin g(s) keeps riding on the standard reward field; the
env supplies l(s) per step via info["l_x"], and the buffer captures it
itself in :meth:ReachAvoidReplayBuffer.add. The avoid and cumulative modes need
no extra column and use SB3's stock ReplayBuffer.
ReachAvoidReplayBuffer ¶
Bases: ReplayBuffer
SB3 replay buffer that also stores the per-step target margin l(s).
l(s) is read from info["l_x"] on each add. Assumes the default
SAC buffer layout (optimize_memory_usage=False), which stores
next_observations explicitly.
Source code in safety_sb3/buffers_replay.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |