SaltedLemon commited on
Commit
d0c9d97
·
verified ·
1 Parent(s): ec0d725

Upload code/solver.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. code/solver.py +163 -0
code/solver.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Adapter exposing the trained planner as a ``stable_worldmodel`` solver.
2
+
3
+ Mirrors :class:`lejepa_control.solver.ControllerSolver` so the planner drops
4
+ into ``WorldModelPolicy`` wherever the baseline controller or ``CEMSolver``
5
+ goes. Same env, same wrappers, same preprocessing, same receding-horizon
6
+ execution — the only thing that differs is how the plan is produced, which is
7
+ what makes the baseline comparison a fair swap.
8
+ """
9
+
10
+ import gymnasium as gym
11
+ import torch
12
+
13
+ from lejepa_control_2.planner import RecursivePlanner
14
+
15
+
16
+ class PlannerSolver:
17
+ """Runs the three-loop recursion instead of CEM's sampling loop.
18
+
19
+ Args:
20
+ model: The frozen ``LeWM``.
21
+ planner: A trained :class:`RecursivePlanner`.
22
+ device: Device to plan on.
23
+ cycles / inner: Override ``T`` / ``n`` at eval time. This is the
24
+ anytime-inference sweep (ablation 1): the recursion is weight-tied,
25
+ so the same checkpoint can be run at any depth. ``None`` keeps the
26
+ trained values.
27
+ """
28
+
29
+ def __init__(self, model, planner, device='cuda', cycles=None, inner=None):
30
+ self.model = model
31
+ self.planner = planner.to(device).eval()
32
+ self.device = device
33
+ self.cycles = cycles
34
+ self.inner = inner
35
+ self._n_envs = 1
36
+ self._horizon = planner.horizon
37
+ self._action_dim = planner.action_dim
38
+ self._action_block = planner.frameskip
39
+
40
+ def configure(self, *, action_space: gym.Space, n_envs: int, config) -> None:
41
+ self._n_envs = n_envs
42
+ self._horizon = config.horizon
43
+ self._action_block = config.action_block
44
+ self._action_dim = int(action_space.shape[-1])
45
+
46
+ assert self._action_block == self.planner.frameskip, (
47
+ f'action_block {self._action_block} != planner frameskip '
48
+ f'{self.planner.frameskip}'
49
+ )
50
+
51
+ @property
52
+ def action_dim(self) -> int:
53
+ return self._action_dim * self._action_block
54
+
55
+ @property
56
+ def n_envs(self) -> int:
57
+ return self._n_envs
58
+
59
+ @property
60
+ def horizon(self) -> int:
61
+ return self._horizon
62
+
63
+ def _encode(self, pixels):
64
+ with torch.no_grad():
65
+ return self.model.encode({'pixels': pixels.to(self.device)})['emb']
66
+
67
+ @torch.no_grad()
68
+ def solve(self, info_dict: dict, init_action=None) -> dict:
69
+ """Plan for every env in ``info_dict``; returns ``{'actions': ...}``."""
70
+ pixels = info_dict['pixels']
71
+ if pixels.ndim == 4: # (B, C, H, W) -> single context frame
72
+ pixels = pixels.unsqueeze(1)
73
+ B, T = pixels.shape[:2]
74
+
75
+ ctx = self._encode(pixels)
76
+
77
+ goal = info_dict['goal']
78
+ if goal.ndim == 4:
79
+ goal = goal.unsqueeze(1)
80
+ goal_emb = self._encode(goal)[:, -1]
81
+
82
+ num_context = self.planner.num_context
83
+ if T < num_context: # early in an episode: repeat the oldest frame
84
+ pad = ctx[:, :1].expand(B, num_context - T, -1)
85
+ ctx = torch.cat([pad, ctx], dim=1)
86
+ elif T > num_context:
87
+ ctx = ctx[:, -num_context:]
88
+
89
+ block_dim = self._action_block * self._action_dim
90
+ past = info_dict.get('action_history')
91
+ if past is None:
92
+ past = ctx.new_zeros(B, num_context - 1, block_dim)
93
+ else:
94
+ past = past.to(self.device).float()
95
+ if past.size(1) < num_context - 1:
96
+ pad = past.new_zeros(
97
+ B, num_context - 1 - past.size(1), block_dim
98
+ )
99
+ past = torch.cat([pad, past], dim=1)
100
+ else:
101
+ past = past[:, -(num_context - 1) :]
102
+ past = torch.nan_to_num(past, 0.0)
103
+
104
+ out = self.planner(
105
+ self.model,
106
+ ctx,
107
+ past,
108
+ goal_emb,
109
+ horizon=self._horizon,
110
+ cycles=self.cycles,
111
+ inner=self.inner,
112
+ )
113
+
114
+ actions = out['blocks'] # (B, H, block*d_a)
115
+ terminal = out['distances'][:, -1]
116
+
117
+ result = {
118
+ 'actions': actions.detach().float().cpu(),
119
+ 'costs': terminal.detach().float().cpu(),
120
+ 'terminal_distance': terminal.mean().item(),
121
+ }
122
+ if out['cycle_distances'] is not None:
123
+ # the stage-B diagnostic, measured at deployment rather than on
124
+ # the training distribution
125
+ result['per_cycle'] = (
126
+ out['cycle_distances'].mean(dim=(0, 1)).detach().cpu().tolist()
127
+ )
128
+ return result
129
+
130
+ __call__ = solve
131
+
132
+
133
+ def load_planner(path, device='cuda', cycles=None, inner=None, horizon=None):
134
+ """Rebuild a planner from a training checkpoint."""
135
+ ckpt = torch.load(path, map_location=device, weights_only=False)
136
+ saved = ckpt['args']
137
+ a_mean = torch.tensor(ckpt['action_mean'])
138
+ a_std = torch.tensor(ckpt['action_std'])
139
+
140
+ planner = RecursivePlanner(
141
+ latent_dim=ckpt['latent_dim'],
142
+ width=saved['width'],
143
+ hidden=saved['hidden'],
144
+ inner=saved['inner'],
145
+ cycles=saved['cycles'],
146
+ horizon=ckpt.get('horizon', saved.get('horizon', 5)),
147
+ use_feedback=saved['use_feedback'],
148
+ warm_start=saved['warm_start'],
149
+ lambda_z=saved['lambda_z'],
150
+ learn_lambda_z=saved['learn_lambda_z'],
151
+ detach_schedule=saved['detach_schedule'],
152
+ action_center=(-a_mean / a_std),
153
+ action_scale=(1.0 / a_std),
154
+ )
155
+ planner.load_state_dict(ckpt['state_dict'])
156
+ planner.to(device).eval()
157
+ if cycles is not None:
158
+ planner.cycles = cycles
159
+ if inner is not None:
160
+ planner.inner = inner
161
+ if horizon is not None:
162
+ planner.horizon = horizon
163
+ return planner, ckpt