Skip to content

Commit b1127ba

Browse files
committed
fix: update init_action handling in CEMSolver and LagrangianSolver tests to ensure correct tensor shapes and values
1 parent fa3e4e1 commit b1127ba

3 files changed

Lines changed: 112 additions & 60 deletions

File tree

tests/solver/test_continuous.py

Lines changed: 101 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Tests for continuous solvers (CEM, MPPI, GD, Nevergrad)."""
22

33
import numpy as np
4-
import pytest
54
import torch
65
from gymnasium import spaces as gym_spaces
76

@@ -15,7 +14,9 @@
1514
class DummyCostModel:
1615
"""Simple Costable implementation for tests."""
1716

18-
def get_cost(self, info_dict: dict, action_candidates: torch.Tensor) -> torch.Tensor:
17+
def get_cost(
18+
self, info_dict: dict, action_candidates: torch.Tensor
19+
) -> torch.Tensor:
1920
# Quadratic cost: sum over horizon and action dims
2021
cost = action_candidates.pow(2).sum(dim=(-1, -2))
2122
return cost
@@ -39,7 +40,9 @@ def test_cem_solver_configure():
3940
"""Test CEMSolver configuration."""
4041
model = DummyCostModel()
4142
solver = CEMSolver(model=model, n_steps=10)
42-
action_space = gym_spaces.Box(low=-1, high=1, shape=(4, 2), dtype=np.float32)
43+
action_space = gym_spaces.Box(
44+
low=-1, high=1, shape=(4, 2), dtype=np.float32
45+
)
4346
config = PlanConfig(horizon=5, receding_horizon=3, action_block=1)
4447

4548
solver.configure(action_space=action_space, n_envs=2, config=config)
@@ -58,48 +61,60 @@ def test_cem_solver_configure_discrete_warning(caplog):
5861
config = PlanConfig(horizon=5, receding_horizon=3)
5962

6063
solver.configure(action_space=action_space, n_envs=1, config=config)
61-
assert "discrete" in caplog.text.lower() or solver._configured # warning logged
64+
assert (
65+
'discrete' in caplog.text.lower() or solver._configured
66+
) # warning logged
6267

6368

6469
def test_cem_solver_init_action_distrib():
6570
"""Test CEMSolver action distribution initialization."""
6671
model = DummyCostModel()
6772
solver = CEMSolver(model=model, n_steps=10)
68-
action_space = gym_spaces.Box(low=-1, high=1, shape=(2, 3), dtype=np.float32)
73+
action_space = gym_spaces.Box(
74+
low=-1, high=1, shape=(2, 3), dtype=np.float32
75+
)
6976
config = PlanConfig(horizon=5, receding_horizon=3)
7077
solver.configure(action_space=action_space, n_envs=2, config=config)
7178

72-
mean, var = solver.init_action_distrib()
79+
actions = torch.zeros(2, 5, 3)
80+
mean, var = solver.init_action_distrib(actions)
7381
assert mean.shape == (2, 5, 3)
7482
assert var.shape == (2, 5, 3)
7583

7684

7785
def test_cem_solver_init_action_distrib_with_init():
78-
"""Test CEMSolver action distribution with initial actions."""
86+
"""Test CEMSolver action distribution returns provided actions as mean."""
7987
model = DummyCostModel()
8088
solver = CEMSolver(model=model, n_steps=10)
81-
action_space = gym_spaces.Box(low=-1, high=1, shape=(2, 3), dtype=np.float32)
89+
action_space = gym_spaces.Box(
90+
low=-1, high=1, shape=(2, 3), dtype=np.float32
91+
)
8292
config = PlanConfig(horizon=5, receding_horizon=3)
8393
solver.configure(action_space=action_space, n_envs=2, config=config)
8494

85-
init_actions = torch.randn(2, 2, 3)
95+
init_actions = torch.randn(2, 5, 3)
8696
mean, var = solver.init_action_distrib(init_actions)
87-
assert mean.shape == (2, 5, 3) # Padded to horizon
97+
assert mean.shape == (2, 5, 3)
98+
assert mean is init_actions
8899

89100

90101
def test_cem_solver_call():
91102
"""Test CEMSolver __call__ method."""
92103
model = DummyCostModel()
93-
solver = CEMSolver(model=model, n_steps=2, num_samples=50, batch_size=2, topk=10)
94-
action_space = gym_spaces.Box(low=-1, high=1, shape=(2, 2), dtype=np.float32)
104+
solver = CEMSolver(
105+
model=model, n_steps=2, num_samples=50, batch_size=2, topk=10
106+
)
107+
action_space = gym_spaces.Box(
108+
low=-1, high=1, shape=(2, 2), dtype=np.float32
109+
)
95110
config = PlanConfig(horizon=3, receding_horizon=2)
96111
solver.configure(action_space=action_space, n_envs=2, config=config)
97112

98-
info_dict = {"pixels": torch.randn(2, 1, 3, 64, 64)}
113+
info_dict = {'pixels': torch.randn(2, 1, 3, 64, 64)}
99114
outputs = solver(info_dict)
100115

101-
assert "actions" in outputs
102-
assert outputs["actions"].shape == (2, 3, 2)
116+
assert 'actions' in outputs
117+
assert outputs['actions'].shape == (2, 3, 2)
103118

104119

105120
###########################
@@ -110,7 +125,9 @@ def test_cem_solver_call():
110125
def test_icem_solver_init():
111126
"""Test ICEMSolver initialization."""
112127
model = DummyCostModel()
113-
solver = ICEMSolver(model=model, n_steps=10, num_samples=100, noise_beta=2.0)
128+
solver = ICEMSolver(
129+
model=model, n_steps=10, num_samples=100, noise_beta=2.0
130+
)
114131
assert solver.model is model
115132
assert solver.n_steps == 10
116133
assert solver.num_samples == 100
@@ -123,7 +140,9 @@ def test_icem_solver_configure():
123140
"""Test ICEMSolver configuration."""
124141
model = DummyCostModel()
125142
solver = ICEMSolver(model=model, n_steps=10)
126-
action_space = gym_spaces.Box(low=-1, high=1, shape=(4, 2), dtype=np.float32)
143+
action_space = gym_spaces.Box(
144+
low=-1, high=1, shape=(4, 2), dtype=np.float32
145+
)
127146
config = PlanConfig(horizon=5, receding_horizon=3, action_block=1)
128147

129148
solver.configure(action_space=action_space, n_envs=2, config=config)
@@ -140,43 +159,59 @@ def test_icem_solver_init_action_distrib():
140159
"""Test ICEMSolver action distribution initialization."""
141160
model = DummyCostModel()
142161
solver = ICEMSolver(model=model, n_steps=10)
143-
action_space = gym_spaces.Box(low=-1, high=1, shape=(2, 3), dtype=np.float32)
162+
action_space = gym_spaces.Box(
163+
low=-1, high=1, shape=(2, 3), dtype=np.float32
164+
)
144165
config = PlanConfig(horizon=5, receding_horizon=3)
145166
solver.configure(action_space=action_space, n_envs=2, config=config)
146167

147-
mean, var = solver.init_action_distrib()
168+
actions = torch.zeros(2, 5, 3)
169+
mean, var = solver.init_action_distrib(actions)
148170
assert mean.shape == (2, 5, 3)
149171
assert var.shape == (2, 5, 3)
150172

151173

152174
def test_icem_solver_call():
153175
"""Test ICEMSolver __call__ method."""
154176
model = DummyCostModel()
155-
solver = ICEMSolver(model=model, n_steps=2, num_samples=50, batch_size=2, topk=10)
156-
action_space = gym_spaces.Box(low=-1, high=1, shape=(2, 2), dtype=np.float32)
177+
solver = ICEMSolver(
178+
model=model, n_steps=2, num_samples=50, batch_size=2, topk=10
179+
)
180+
action_space = gym_spaces.Box(
181+
low=-1, high=1, shape=(2, 2), dtype=np.float32
182+
)
157183
config = PlanConfig(horizon=3, receding_horizon=2)
158184
solver.configure(action_space=action_space, n_envs=2, config=config)
159185

160-
info_dict = {"pixels": torch.randn(2, 1, 3, 64, 64)}
186+
info_dict = {'pixels': torch.randn(2, 1, 3, 64, 64)}
161187
outputs = solver(info_dict)
162188

163-
assert "actions" in outputs
164-
assert outputs["actions"].shape == (2, 3, 2)
189+
assert 'actions' in outputs
190+
assert outputs['actions'].shape == (2, 3, 2)
165191

166192

167193
def test_icem_solver_white_noise_fallback():
168194
"""Test ICEMSolver with beta=0 (white noise, equivalent to standard CEM)."""
169195
model = DummyCostModel()
170-
solver = ICEMSolver(model=model, n_steps=2, num_samples=50, batch_size=2, topk=10, noise_beta=0.0)
171-
action_space = gym_spaces.Box(low=-1, high=1, shape=(2, 2), dtype=np.float32)
196+
solver = ICEMSolver(
197+
model=model,
198+
n_steps=2,
199+
num_samples=50,
200+
batch_size=2,
201+
topk=10,
202+
noise_beta=0.0,
203+
)
204+
action_space = gym_spaces.Box(
205+
low=-1, high=1, shape=(2, 2), dtype=np.float32
206+
)
172207
config = PlanConfig(horizon=3, receding_horizon=2)
173208
solver.configure(action_space=action_space, n_envs=2, config=config)
174209

175-
info_dict = {"pixels": torch.randn(2, 1, 3, 64, 64)}
210+
info_dict = {'pixels': torch.randn(2, 1, 3, 64, 64)}
176211
outputs = solver(info_dict)
177212

178-
assert "actions" in outputs
179-
assert outputs["actions"].shape == (2, 3, 2)
213+
assert 'actions' in outputs
214+
assert outputs['actions'].shape == (2, 3, 2)
180215

181216

182217
###########################
@@ -197,7 +232,9 @@ def test_mppi_solver_configure():
197232
"""Test MPPISolver configuration."""
198233
model = DummyCostModel()
199234
solver = MPPISolver(model=model, n_steps=10)
200-
action_space = gym_spaces.Box(low=-1, high=1, shape=(4, 2), dtype=np.float32)
235+
action_space = gym_spaces.Box(
236+
low=-1, high=1, shape=(4, 2), dtype=np.float32
237+
)
201238
config = PlanConfig(horizon=5, receding_horizon=3)
202239

203240
solver.configure(action_space=action_space, n_envs=2, config=config)
@@ -212,11 +249,14 @@ def test_mppi_solver_init_action_distrib():
212249
"""Test MPPISolver action distribution initialization."""
213250
model = DummyCostModel()
214251
solver = MPPISolver(model=model, n_steps=10)
215-
action_space = gym_spaces.Box(low=-1, high=1, shape=(2, 3), dtype=np.float32)
252+
action_space = gym_spaces.Box(
253+
low=-1, high=1, shape=(2, 3), dtype=np.float32
254+
)
216255
config = PlanConfig(horizon=5, receding_horizon=3)
217256
solver.configure(action_space=action_space, n_envs=2, config=config)
218257

219-
mean, var = solver.init_action_distrib()
258+
actions = torch.zeros(2, 5, 3)
259+
mean, var = solver.init_action_distrib(actions)
220260
assert mean.shape == (2, 5, 3)
221261
assert var.shape == (2, 5, 3)
222262

@@ -225,15 +265,17 @@ def test_mppi_solver_call():
225265
"""Test MPPISolver __call__ method."""
226266
model = DummyCostModel()
227267
solver = MPPISolver(model=model, n_steps=2, num_samples=10, batch_size=2)
228-
action_space = gym_spaces.Box(low=-1, high=1, shape=(2, 2), dtype=np.float32)
268+
action_space = gym_spaces.Box(
269+
low=-1, high=1, shape=(2, 2), dtype=np.float32
270+
)
229271
config = PlanConfig(horizon=3, receding_horizon=2)
230272
solver.configure(action_space=action_space, n_envs=2, config=config)
231273

232-
info_dict = {"pixels": torch.randn(2, 1, 3, 64, 64)}
274+
info_dict = {'pixels': torch.randn(2, 1, 3, 64, 64)}
233275
outputs = solver(info_dict)
234276

235-
assert "actions" in outputs
236-
assert outputs["actions"].shape == (2, 3, 2)
277+
assert 'actions' in outputs
278+
assert outputs['actions'].shape == (2, 3, 2)
237279

238280

239281
###########################
@@ -254,7 +296,9 @@ def test_gradient_solver_configure():
254296
"""Test GradientSolver configuration."""
255297
model = DummyCostModel()
256298
solver = GradientSolver(model=model, n_steps=10)
257-
action_space = gym_spaces.Box(low=-1, high=1, shape=(4, 2), dtype=np.float32)
299+
action_space = gym_spaces.Box(
300+
low=-1, high=1, shape=(4, 2), dtype=np.float32
301+
)
258302
config = PlanConfig(horizon=5, receding_horizon=3)
259303

260304
solver.configure(action_space=action_space, n_envs=2, config=config)
@@ -269,25 +313,36 @@ def test_gradient_solver_init_action():
269313
"""Test GradientSolver action initialization."""
270314
model = DummyCostModel()
271315
solver = GradientSolver(model=model, n_steps=10, num_samples=3)
272-
action_space = gym_spaces.Box(low=-1, high=1, shape=(2, 3), dtype=np.float32)
316+
action_space = gym_spaces.Box(
317+
low=-1, high=1, shape=(2, 3), dtype=np.float32
318+
)
273319
config = PlanConfig(horizon=5, receding_horizon=3)
274320
solver.configure(action_space=action_space, n_envs=2, config=config)
275321

276-
solver.init_action()
277-
assert hasattr(solver, "init")
278-
assert solver.init.shape == (2, 3, 5, 3) # (n_envs, num_samples, horizon, action_dim)
322+
solver.init_action(torch.zeros(2, 5, 3))
323+
assert hasattr(solver, 'init')
324+
assert solver.init.shape == (
325+
2,
326+
3,
327+
5,
328+
3,
329+
) # (n_envs, num_samples, horizon, action_dim)
279330

280331

281332
def test_gradient_solver_call():
282333
"""Test GradientSolver __call__ method."""
283334
model = DummyCostModel()
284-
solver = GradientSolver(model=model, n_steps=2, num_samples=2, batch_size=2)
285-
action_space = gym_spaces.Box(low=-1, high=1, shape=(2, 2), dtype=np.float32)
335+
solver = GradientSolver(
336+
model=model, n_steps=2, num_samples=2, batch_size=2
337+
)
338+
action_space = gym_spaces.Box(
339+
low=-1, high=1, shape=(2, 2), dtype=np.float32
340+
)
286341
config = PlanConfig(horizon=3, receding_horizon=2)
287342
solver.configure(action_space=action_space, n_envs=2, config=config)
288343

289-
info_dict = {"pixels": torch.randn(2, 1, 3, 64, 64)}
344+
info_dict = {'pixels': torch.randn(2, 1, 3, 64, 64)}
290345
outputs = solver(info_dict)
291346

292-
assert "actions" in outputs
293-
assert outputs["actions"].shape == (2, 3, 2)
347+
assert 'actions' in outputs
348+
assert outputs['actions'].shape == (2, 3, 2)

tests/solver/test_lagrangian.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
"""Tests for LagrangianSolver class."""
22

3-
import dataclasses
4-
53
import numpy as np
64
import pytest
75
import torch
8-
import torch.nn.functional as F
96
from gymnasium import spaces as gym_spaces
107

118
from stable_worldmodel.policy import PlanConfig
@@ -170,31 +167,30 @@ def test_n_envs_before_configure_is_none():
170167
###########################
171168

172169

173-
def test_init_action_none_creates_zeros():
174-
"""init_action(None) creates zero tensor of correct shape."""
170+
def test_init_action_zeros_creates_zero_init():
171+
"""init_action with a zeros tensor creates a zero init of correct shape."""
175172
solver = make_solver(num_samples=1, var_scale=0.0)
176173
configure(solver, action_dim=4, n_envs=2, horizon=5, action_block=1)
177174

178175
with torch.no_grad():
179-
solver.init_action(None)
176+
solver.init_action(torch.zeros(2, 5, 4))
180177

181178
# shape: (n_envs, num_samples, horizon, action_dim)
182179
assert solver.init.shape == (2, 1, 5, 4)
183180
assert torch.allclose(solver.init, torch.zeros_like(solver.init))
184181

185182

186-
def test_init_action_partial_fills_remaining_with_zeros():
187-
"""Providing fewer steps pads the rest with zeros (first sample only)."""
183+
def test_init_action_full_tensor_preserved_as_first_sample():
184+
"""Providing a full-horizon tensor preserves it as the first sample."""
188185
solver = make_solver(num_samples=1, var_scale=0.0)
189186
configure(solver, action_dim=4, n_envs=2, horizon=6, action_block=1)
190187

191-
init = torch.ones(2, 3, 4) # 3 of 6 steps
188+
init = torch.ones(2, 6, 4)
192189
with torch.no_grad():
193190
solver.init_action(init)
194191

195192
first_sample = solver.init[:, 0] # (n_envs, horizon, action_dim)
196-
assert torch.allclose(first_sample[:, :3], init)
197-
assert torch.allclose(first_sample[:, 3:], torch.zeros(2, 3, 4))
193+
assert torch.allclose(first_sample, init)
198194

199195

200196
def test_init_action_full_horizon_preserved():
@@ -449,7 +445,7 @@ def test_persist_multipliers_warm_starts():
449445
configure(solver, action_dim=4, n_envs=2, horizon=4, action_block=1)
450446

451447
out1 = solver.solve({})
452-
lambdas_after_first = out1['lambdas'].clone()
448+
_lambdas_after_first = out1['lambdas'].clone()
453449

454450
out2 = solver.solve({})
455451
lambdas_after_second = out2['lambdas']

tests/test_policy.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ def test_worldmodel_policy_warmstart_extends_partial_plan(actionable_setup):
723723

724724

725725
def test_worldmodel_policy_no_warmstart_without_actionable():
726-
"""Solver receives None init_action when model does not implement Actionable."""
726+
"""Solver receives a full zero init_action when model does not implement Actionable."""
727727
non_actionable_model = MagicMock(spec=['get_cost'])
728728
solver = MockSolverWithWarmStart(model=non_actionable_model)
729729
config = PlanConfig(
@@ -741,7 +741,8 @@ def test_worldmodel_policy_no_warmstart_without_actionable():
741741

742742
policy.get_action(info)
743743

744-
assert solver.received_init_action is None
744+
assert solver.received_init_action.shape == (1, 5, 2)
745+
assert solver.received_init_action.eq(0).all()
745746

746747

747748
###########################

0 commit comments

Comments
 (0)