Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/misc/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

# Changelog

## Release 2.9.2a0 (2026-07-18)

### Breaking Changes:

### New Features:

### Bug Fixes:

### [SB3-Contrib]

### [RL Zoo]

### [SBX] (SB3 + Jax)

### Deprecations:

### Others:

- Fixed moviepy test compatibility with moviepy >= 2.0 (PyTorch doesn't support it yet)
- Added filter for numpy `newshape` deprecation warning from torch tensorboard
- Fixed mypy error in test_vec_envs.py by wrapping `itertools.product` with `list()`
- Improved tests coverage for RMSpropTFLike optimizer (invalid params, centered/momentum/weight_decay branches, pickle roundtrip)
- Improved test coverage for `save_util.py`: added tests for `BadZipFile` error handling in `load_from_zip_file` and `IsADirectoryError`/`FileNotFoundError` handling in `open_path`

### Documentation:

## Release 2.9.0 (2026-06-15)

**Updated dependencies (pandas is now optional, gymnasium 1.3.0 support, torch>=2.8)**
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ filterwarnings = [
"ignore:You are trying to run (PPO|A2C) on the GPU",
# Tensorboard warnings
"ignore::DeprecationWarning:tensorboard",
# Torch tensorboard numpy newshape deprecation
"ignore:`newshape` keyword argument is deprecated:DeprecationWarning:torch.utils.tensorboard._utils",
# Gymnasium warnings
"ignore::UserWarning:gymnasium",
# Taxi-v3 and CliffWalking-v0
Expand Down
6 changes: 3 additions & 3 deletions stable_baselines3/common/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def obs_to_tensor(self, observation: np.ndarray | dict[str, np.ndarray]) -> tupl
obs_ = np.array(obs)
vectorized_env = vectorized_env or is_vectorized_observation(obs_, obs_space)
# Add batch dimension if needed
observation[key] = obs_.reshape((-1, *self.observation_space[key].shape)) # type: ignore[misc]
observation[key] = obs_.reshape((-1, *self.observation_space[key].shape)) # type: ignore[misc, arg-type]

elif is_image_space(self.observation_space):
# Handle the different cases for images
Expand All @@ -271,7 +271,7 @@ def obs_to_tensor(self, observation: np.ndarray | dict[str, np.ndarray]) -> tupl
# Dict obs need to be handled separately
vectorized_env = is_vectorized_observation(observation, self.observation_space)
# Add batch dimension if needed
observation = observation.reshape((-1, *self.observation_space.shape)) # type: ignore[misc]
observation = observation.reshape((-1, *self.observation_space.shape)) # type: ignore[misc, arg-type]

obs_tensor = obs_as_tensor(observation, self.device)
return obs_tensor, vectorized_env
Expand Down Expand Up @@ -367,7 +367,7 @@ def predict(
with th.no_grad():
actions = self._predict(obs_tensor, deterministic=deterministic)
# Convert to numpy, and reshape to the original action shape
actions = actions.cpu().numpy().reshape((-1, *self.action_space.shape)) # type: ignore[misc, assignment]
actions = actions.cpu().numpy().reshape((-1, *self.action_space.shape)) # type: ignore[misc, assignment, arg-type]

if isinstance(self.action_space, spaces.Box):
if self.squash_output:
Expand Down
2 changes: 1 addition & 1 deletion stable_baselines3/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.9.0
2.9.2a0
10 changes: 8 additions & 2 deletions tests/test_logger.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import importlib.util
import json
import os
import sys
Expand Down Expand Up @@ -273,7 +272,14 @@ def test_report_video_to_tensorboard(tmp_path, read_log, capsys):


def is_moviepy_installed():
return importlib.util.find_spec("moviepy") is not None
try:
import moviepy

# PyTorch doesn't support moviepy >= 2.0
# See: https://github.com/pytorch/pytorch/issues/147317
return moviepy.__version__ < "2.0"
except (ImportError, AttributeError):
return False


@pytest.mark.parametrize("unsupported_format", ["stdout", "log", "json", "csv"])
Expand Down
31 changes: 31 additions & 0 deletions tests/test_save_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,3 +891,34 @@ def test_save_load_clip_range_portable(tmp_path, model_class):
assert isinstance(model.clip_range, FloatSchedule)
assert isinstance(model.clip_range.value_schedule, ConstantSchedule)
assert model.clip_range.value_schedule.val == 0.2


def test_load_bad_zip(tmp_path):
"""Test that loading a non-zip file raises a ValueError."""
# Write a plain text file to the path
bad_path = tmp_path / "not_a_zip.zip"
bad_path.write_text("this is not a zip file")

with pytest.raises(ValueError, match="wasn't a zip-file"):
PPO.load(str(bad_path))


@pytest.mark.filterwarnings("error::ResourceWarning")
def test_open_path_directory_and_missing_parent(tmp_path):
"""Test IsADirectoryError and FileNotFoundError handling in open_path."""
# Test IsADirectoryError: path already has the suffix and is a folder
dir_path = tmp_path / "is_a_folder.pkl"
dir_path.mkdir()
with warnings.catch_warnings(record=True) as record:
open_path(dir_path, "w", suffix="pkl").close()
assert any("is a folder" in str(warning.message) for warning in record)
# Verify the fallback file was created
assert (tmp_path / "is_a_folder.pkl_2").exists()

# Test FileNotFoundError: parent folder doesn't exist
missing_parent_path = tmp_path / "nonexistent_parent" / "sub" / "file"
with warnings.catch_warnings(record=True) as record:
open_path(missing_parent_path, "w", suffix="pkl").close()
assert any("does not exist" in str(warning.message) for warning in record)
# Verify the parent was created and file was written
assert (tmp_path / "nonexistent_parent" / "sub" / "file.pkl").exists()
56 changes: 56 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import pickle
import shutil

import ale_py
Expand All @@ -15,6 +16,7 @@
from stable_baselines3.common.evaluation import evaluate_policy
from stable_baselines3.common.monitor import Monitor
from stable_baselines3.common.noise import OrnsteinUhlenbeckActionNoise, VectorizedActionNoise
from stable_baselines3.common.sb2_compat.rmsprop_tf_like import RMSpropTFLike
from stable_baselines3.common.utils import (
ConstantSchedule,
FloatSchedule,
Expand Down Expand Up @@ -628,3 +630,57 @@ def test_deprecated_schedules():
assert np.allclose(fn(0.5), schedule(0.5))
assert np.allclose(fn(0.5), float_schedule(0.5))
assert np.allclose(fn(0.5), float_schedule_2(0.5))


class TestRMSpropTFLike:
"""Tests for the RMSpropTFLike optimizer (sb2_compat)."""

def test_invalid_params(self):
"""Test that invalid optimizer parameters raise ValueError."""
param = th.nn.Parameter(th.zeros(2))
with pytest.raises(ValueError, match="Invalid learning rate"):
RMSpropTFLike([param], lr=-1.0)
with pytest.raises(ValueError, match="Invalid epsilon"):
RMSpropTFLike([param], eps=-1.0)
with pytest.raises(ValueError, match="Invalid momentum"):
RMSpropTFLike([param], momentum=-1.0)
with pytest.raises(ValueError, match="Invalid weight_decay"):
RMSpropTFLike([param], weight_decay=-1.0)
with pytest.raises(ValueError, match="Invalid alpha"):
RMSpropTFLike([param], alpha=-1.0)

def test_centered_momentum_weight_decay(self):
"""Test centered=True, momentum>0, weight_decay>0 branches in step()."""
param = th.nn.Parameter(th.tensor([1.0, 2.0]))
opt = RMSpropTFLike(
[param],
lr=0.1,
momentum=0.9,
alpha=0.99,
eps=1e-8,
centered=True,
weight_decay=0.01,
)
# Compute gradient
loss = (param**2).sum()
loss.backward()

# Run step with closure (covers closure branch)
def closure():
param.grad.zero_()
return float((param**2).sum().detach())

opt.step(closure=closure)
# Check state keys exist (covers centered + momentum init)
state = opt.state[param]
assert "grad_avg" in state # centered=True
assert "momentum_buffer" in state # momentum > 0

def test_pickle_roundtrip(self):
"""Test that optimizer survives pickle roundtrip (covers __setstate__)."""
param = th.nn.Parameter(th.tensor([1.0, 2.0]))
opt = RMSpropTFLike([param], lr=0.1, momentum=0.5, centered=True)
opt2 = pickle.loads(pickle.dumps(opt))
assert opt2.param_groups[0]["lr"] == 0.1
assert opt2.param_groups[0]["momentum"] == 0.5
assert opt2.param_groups[0]["centered"] is True
2 changes: 1 addition & 1 deletion tests/test_vec_envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def check_vecenv_obs(obs, space):
assert space.contains(value)


@pytest.mark.parametrize("vec_env_class,space", itertools.product(VEC_ENV_CLASSES, SPACES.values()))
@pytest.mark.parametrize("vec_env_class,space", list(itertools.product(VEC_ENV_CLASSES, SPACES.values())))
def test_vecenv_single_space(vec_env_class, space):
def obs_assert(obs):
return check_vecenv_obs(obs, space)
Expand Down