VAstaf commited on
Commit
cd3a2b4
·
verified ·
1 Parent(s): 9011631

Upload 2 files

Browse files
Files changed (2) hide show
  1. ACTmodel.py +725 -0
  2. so101_scene.xml +198 -0
ACTmodel.py ADDED
@@ -0,0 +1,725 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # ACT (Action Chunking Transformer) Policy для LeRobot.
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import Optional, Any
6
+ import torch
7
+
8
+ @dataclass
9
+ class ACTConfig:
10
+ # Архитектура трансформера
11
+ dim_model: int = 512
12
+ n_heads: int = 8
13
+ dim_feedforward: int = 3200
14
+ n_encoder_layers: int = 4
15
+ n_decoder_layers: int = 1
16
+
17
+ # ACT-специфичные
18
+ chunk_size: int = 100
19
+ latent_dim: int = 32
20
+ use_vae: bool = True
21
+ temporal_ensemble_coeff: Optional[float] = 0.1
22
+ n_action_steps: int = 1
23
+
24
+ # Параметры VAE (именно их не хватает)
25
+ n_vae_encoder_layers = 4 # <-- обязательно
26
+ # use_vae = False # <-- важно, если используешь VAE-ветку
27
+
28
+ # Активации и нормализация
29
+ feedforward_activation: str = "relu"
30
+ pre_norm: bool = True
31
+ dropout: float = 0.1
32
+
33
+ # Признаки (должны быть тензорами, чтобы инициализировать Linear)
34
+ robot_state_feature: Optional[torch.Tensor] = None
35
+ env_state_feature: Optional[torch.Tensor] = None
36
+ action_feature: Optional[torch.Tensor] = None
37
+
38
+ # Изображения (можно оставить None для теста без картинок)
39
+ image_features: bool = True
40
+ vision_backbone: str = "resnet18"
41
+ pretrained_backbone_weights: str | None = "ResNet18_Weights.IMAGENET1K_V1"
42
+ replace_final_stride_with_dilation: bool = False
43
+
44
+ def validate_features(self):
45
+ # заглушка, чтобы не ломался вызов config.validate_features() в ACTPolicy
46
+ pass
47
+
48
+ import math
49
+ from collections import deque
50
+ from collections.abc import Callable
51
+ from itertools import chain
52
+
53
+ import einops
54
+ import numpy as np
55
+ import torch
56
+ import torch.nn.functional as F
57
+ import torchvision
58
+ from torch import Tensor, nn
59
+ from torchvision.models._utils import IntermediateLayerGetter
60
+ from torchvision.ops.misc import FrozenBatchNorm2d
61
+
62
+ OBS_STATE = "observation.state"
63
+ ACTION = "action"
64
+ OBS_ENV_STATE = "env_state"
65
+ OBS_IMAGES = "observation.images.realsense"
66
+
67
+
68
+
69
+ def create_sinusoidal_pos_embedding(num_positions: int, dimension: int) -> Tensor:
70
+
71
+ def get_position_angle_vec(position: int):
72
+ # Вычисляем углы для каждой размерности
73
+ return [
74
+ position / np.power(10000, 2 * (hid_j // 2) / dimension)
75
+ for hid_j in range(dimension)
76
+ ]
77
+
78
+ # Строим таблицу: каждая строка — позиция, каждый столбец — размерность
79
+ sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(num_positions)])
80
+
81
+ # Применяем sin к чётным индексам, cos — к нечётным
82
+ sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2])
83
+ sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2])
84
+
85
+ return torch.from_numpy(sinusoid_table).float()
86
+
87
+
88
+ class ACTSinusoidalPositionEmbedding2d(nn.Module):
89
+
90
+ def __init__(self, dimension: int):
91
+ super().__init__()
92
+ self.dimension = dimension
93
+ self._two_pi = 2 * math.pi
94
+ self._eps = 1e-6
95
+ self._temperature = 10000 # стандартная константа для частоты синусоид
96
+
97
+ def forward(self, x: Tensor) -> Tensor:
98
+ # Создаём маску-заглушку, чтобы получить размеры H и W
99
+ not_mask = torch.ones_like(x[0, :1]) # (1, H, W)
100
+
101
+ # Накопительная сумма даёт индексы строк/столбцов: [1..H], [1..W]
102
+ y_range = not_mask.cumsum(1, dtype=torch.float32)
103
+ x_range = not_mask.cumsum(2, dtype=torch.float32)
104
+
105
+ # Нормализуем в [0, 2π]
106
+ y_range = y_range / (y_range[:, -1:, :] + self._eps) * self._two_pi
107
+ x_range = x_range / (x_range[:, :, -1:] + self._eps) * self._two_pi
108
+
109
+ # Частоты синусоид (как в оригинальной статье про трансформеры)
110
+ inverse_frequency = self._temperature ** (
111
+ 2 * (torch.arange(self.dimension, dtype=torch.float32, device=x.device) // 2)
112
+ / self.dimension
113
+ )
114
+
115
+ # Добавляем измерение для broadcasting
116
+ x_range = x_range.unsqueeze(-1) / inverse_frequency # (1, H, W, C//2)
117
+ y_range = y_range.unsqueeze(-1) / inverse_frequency
118
+
119
+ # Чередуем sin/cos для чётных/нечётных размерностей
120
+ pos_embed_x = torch.stack(
121
+ (x_range[..., 0::2].sin(), x_range[..., 1::2].cos()), dim=-1
122
+ ).flatten(3)
123
+ pos_embed_y = torch.stack(
124
+ (y_range[..., 0::2].sin(), y_range[..., 1::2].cos()), dim=-1
125
+ ).flatten(3)
126
+
127
+ # Объединяем и переставляем оси: (1, C, H, W)
128
+ pos_embed = torch.cat((pos_embed_y, pos_embed_x), dim=3).permute(0, 3, 1, 2)
129
+ return pos_embed
130
+
131
+
132
+ def get_activation_fn(activation: str) -> Callable:
133
+ """Возвращает функцию активации по имени (для конфигурации модели)."""
134
+ if activation == "relu":
135
+ return F.relu
136
+ if activation == "gelu":
137
+ return F.gelu
138
+ if activation == "glu":
139
+ return F.glu
140
+ raise RuntimeError(f"activation should be relu/gelu/glu, not {activation}.")
141
+
142
+
143
+ class ACTEncoderLayer(nn.Module):
144
+ """Один слой энкодера трансформера с self-attention и feed-forward."""
145
+
146
+ def __init__(self, config: ACTConfig):
147
+ super().__init__()
148
+ # Multihead self-attention
149
+ self.self_attn = nn.MultiheadAttention(
150
+ config.dim_model, config.n_heads, dropout=config.dropout, batch_first=True
151
+ )
152
+
153
+ # Feed-forward сеть
154
+ self.linear1 = nn.Linear(config.dim_model, config.dim_feedforward)
155
+ self.dropout = nn.Dropout(config.dropout)
156
+ self.linear2 = nn.Linear(config.dim_feedforward, config.dim_model)
157
+
158
+ # LayerNorm'ы и dropout'ы
159
+ self.norm1 = nn.LayerNorm(config.dim_model)
160
+ self.norm2 = nn.LayerNorm(config.dim_model)
161
+ self.dropout1 = nn.Dropout(config.dropout)
162
+ self.dropout2 = nn.Dropout(config.dropout)
163
+
164
+ self.activation = get_activation_fn(config.feedforward_activation)
165
+ self.pre_norm = config.pre_norm # если True, то LayerNorm перед attention
166
+
167
+ def forward(
168
+ self,
169
+ x: Tensor,
170
+ pos_embed: Tensor | None = None,
171
+ key_padding_mask: Tensor | None = None,
172
+ return_internals: bool = False
173
+ ) -> Tensor| tuple[Tensor, dict]:
174
+ skip = x
175
+ internals = {}
176
+ # Pre-Norm: нормализация перед attention
177
+ if self.pre_norm:
178
+ x = self.norm1(x)
179
+
180
+ q = k = x if pos_embed is None else x + pos_embed
181
+ attn_out, attn_weights = self.self_attn(q, k, value=x, key_padding_mask=key_padding_mask)
182
+ x = skip + self.dropout1(attn_out)
183
+
184
+ if return_internals:
185
+ internals["self_attn_weights"] = attn_weights # (B, H, T_enc, T_enc)
186
+ internals["self_attn_out"] = attn_out # (B, T_enc, D)
187
+
188
+ # Дальше — feed-forward
189
+ if self.pre_norm:
190
+ skip = x
191
+ x = self.norm2(x)
192
+ else:
193
+ x = self.norm1(x)
194
+ skip = x
195
+
196
+ ff_pre = x
197
+ x = self.linear2(self.dropout(self.activation(self.linear1(x))))
198
+ x = skip + self.dropout2(x)
199
+
200
+ if not self.pre_norm:
201
+ x = self.norm2(x)
202
+
203
+ if return_internals:
204
+ internals["ff_pre"] = ff_pre
205
+ internals["ff_out"] = x
206
+
207
+ return (x, internals) if return_internals else x
208
+
209
+
210
+ class ACTEncoder(nn.Module):
211
+ """Стек слоёв энкодера. Может быть использован как VAE-энкодер или обычный энкодер."""
212
+
213
+ def __init__(self, config: ACTConfig, is_vae_encoder: bool = False):
214
+ super().__init__()
215
+ num_layers = (
216
+ config.n_vae_encoder_layers
217
+ if is_vae_encoder
218
+ else config.n_encoder_layers
219
+ )
220
+ self.layers = nn.ModuleList([ACTEncoderLayer(config) for _ in range(num_layers)])
221
+ # Последний LayerNorm
222
+ self.norm = nn.LayerNorm(config.dim_model) if config.pre_norm else nn.Identity()
223
+ self.is_vae_encoder = is_vae_encoder
224
+
225
+ def forward(
226
+ self,
227
+ x: Tensor,
228
+ pos_embed: Tensor | None = None,
229
+ key_padding_mask: Tensor | None = None
230
+ ) -> Tensor:
231
+ for layer in self.layers:
232
+ x = layer(x, pos_embed=pos_embed, key_padding_mask=key_padding_mask)
233
+ x = self.norm(x)
234
+ return x
235
+
236
+
237
+ class ACTDecoderLayer(nn.Module):
238
+ """Один слой декодера трансформера: self-attention + cross-attention + feed-forward."""
239
+
240
+ def __init__(self, config: ACTConfig):
241
+ super().__init__()
242
+ self.self_attn = nn.MultiheadAttention(config.dim_model, config.n_heads, dropout=config.dropout, batch_first=True )
243
+ self.multihead_attn = nn.MultiheadAttention(config.dim_model, config.n_heads, dropout=config.dropout, batch_first=True )
244
+
245
+ self.linear1 = nn.Linear(config.dim_model, config.dim_feedforward)
246
+ self.dropout = nn.Dropout(config.dropout)
247
+ self.linear2 = nn.Linear(config.dim_feedforward, config.dim_model)
248
+
249
+ self.norm1 = nn.LayerNorm(config.dim_model)
250
+ self.norm2 = nn.LayerNorm(config.dim_model)
251
+ self.norm3 = nn.LayerNorm(config.dim_model)
252
+ self.dropout1 = nn.Dropout(config.dropout)
253
+ self.dropout2 = nn.Dropout(config.dropout)
254
+ self.dropout3 = nn.Dropout(config.dropout)
255
+
256
+ self.activation = get_activation_fn(config.feedforward_activation)
257
+ self.pre_norm = config.pre_norm
258
+
259
+ def maybe_add_pos_embed(self, tensor: Tensor, pos_embed: Tensor | None) -> Tensor:
260
+ return tensor if pos_embed is None else tensor + pos_embed
261
+
262
+ def forward(
263
+ self,
264
+ x: Tensor,
265
+ encoder_out: Tensor,
266
+ decoder_pos_embed: Tensor | None = None,
267
+ encoder_pos_embed: Tensor | None = None
268
+ ) -> Tensor:
269
+ skip = x
270
+ if self.pre_norm:
271
+ x = self.norm1(x)
272
+
273
+ q = k = self.maybe_add_pos_embed(x, decoder_pos_embed)
274
+ attn_self, _ = self.self_attn(q, k, value=x)
275
+ x = skip + self.dropout1(attn_self)
276
+
277
+ if self.pre_norm:
278
+ skip = x
279
+ x = self.norm2(x)
280
+ else:
281
+ x = self.norm1(x)
282
+ skip = x
283
+ #print ("x.dec: ", x.shape)
284
+ #print("decoder_pos_embed: ", decoder_pos_embed)
285
+ #print("encoder_pos_embed: ", encoder_pos_embed)
286
+
287
+ # Cross-attention к энкодеру
288
+ query = self.maybe_add_pos_embed(x, decoder_pos_embed)
289
+ key = self.maybe_add_pos_embed(encoder_out, encoder_pos_embed)
290
+ value = encoder_out
291
+
292
+
293
+ attn_cross, attn_weights = self.multihead_attn(
294
+ query=query,
295
+ key=key,
296
+ value=value,
297
+ )
298
+
299
+ #print("attn_cross.shape:", attn_cross.shape)
300
+ #print("attn_weights.shape:", attn_weights.shape)
301
+ x = skip + self.dropout2(attn_cross)
302
+
303
+ if self.pre_norm:
304
+ skip = x
305
+ x = self.norm3(x)
306
+ else:
307
+ x = self.norm2(x)
308
+ skip = x
309
+
310
+ x = self.linear2(self.dropout(self.activation(self.linear1(x))))
311
+ x = skip + self.dropout3(x)
312
+
313
+ if not self.pre_norm:
314
+ x = self.norm3(x)
315
+
316
+ return x
317
+
318
+
319
+ class ACTDecoder(nn.Module):
320
+
321
+ def __init__(self, config: ACTConfig):
322
+ super().__init__()
323
+ self.layers = nn.ModuleList([ACTDecoderLayer(config) for _ in range(config.n_decoder_layers)])
324
+ self.norm = nn.LayerNorm(config.dim_model)
325
+
326
+ def forward(
327
+ self,
328
+ x: Tensor,
329
+ encoder_out: Tensor,
330
+ decoder_pos_embed: Tensor | None = None,
331
+ encoder_pos_embed: Tensor | None = None
332
+ ) -> Tensor:
333
+ for layer in self.layers:
334
+ x = layer(
335
+ x,
336
+ encoder_out,
337
+ decoder_pos_embed=decoder_pos_embed,
338
+ encoder_pos_embed=encoder_pos_embed
339
+ )
340
+ if self.norm is not None:
341
+ x = self.norm(x)
342
+ return x
343
+
344
+ class ACTTemporalEnsembler:
345
+
346
+ def __init__(self, temporal_ensemble_coeff: float, chunk_size: int) -> None:
347
+ self.chunk_size = chunk_size
348
+ # Предвычисляем веса и их кумулятивную сумму
349
+ self.ensemble_weights = torch.exp(-temporal_ensemble_coeff * torch.arange(chunk_size))
350
+ self.ensemble_weights_cumsum = torch.cumsum(self.ensemble_weights, dim=0)
351
+ self.reset()
352
+
353
+ def reset(self):
354
+ """Сброс состояния при ресете среды (новый эпизод)."""
355
+ self.ensembled_actions = None
356
+ self.ensembled_actions_count = None
357
+
358
+ def update(self, actions: Tensor) -> Tensor:
359
+
360
+ # Переносим веса на устройство тензора действий
361
+ self.ensemble_weights = self.ensemble_weights.to(device=actions.device)
362
+ self.ensemble_weights_cumsum = self.ensemble_weights_cumsum.to(device=actions.device)
363
+
364
+ if self.ensembled_actions is None:
365
+ # Инициализация: первое предсказание становится начальным ансамблем
366
+ self.ensembled_actions = actions.clone()
367
+ self.ensembled_actions_count = torch.ones(
368
+ (self.chunk_size, 1), dtype=torch.long, device=self.ensembled_actions.device
369
+ )
370
+ else:
371
+ # Онлайн-обновление: взвешенное усреднение с учётом новых предсказаний
372
+ # Это эквивалентно пересчёту среднего с новыми весами без хранения всей истории
373
+ self.ensembled_actions *= self.ensemble_weights_cumsum[self.ensembled_actions_count - 1]
374
+ self.ensembled_actions += actions[:, :-1] * self.ensemble_weights[self.ensembled_actions_count]
375
+ self.ensembled_actions /= self.ensemble_weights_cumsum[self.ensembled_actions_count]
376
+
377
+ # Увеличиваем счётчик количества шагов в ансамбле (не более chunk_size)
378
+ self.ensembled_actions_count = torch.clamp(
379
+ self.ensembled_actions_count + 1, max=self.chunk_size
380
+ )
381
+
382
+ # Добавляем последнее действие из нового чанка как новый элемент в конец последовательности
383
+ self.ensembled_actions = torch.cat([self.ensembled_actions, actions[:, -1:]], dim=1)
384
+ self.ensembled_actions_count = torch.cat(
385
+ [
386
+ self.ensembled_actions_count,
387
+ torch.ones_like(self.ensembled_actions_count[-1:])
388
+ ]
389
+ )
390
+
391
+ # «Потребляем» первое действие из ансамбля и сдвигаем окно вперёд
392
+ action, self.ensembled_actions, self.ensembled_actions_count = (
393
+ self.ensembled_actions[:, 0],
394
+ self.ensembled_actions[:, 1:],
395
+ self.ensembled_actions_count[1:]
396
+ )
397
+
398
+ return action
399
+
400
+
401
+ class ACT(nn.Module):
402
+
403
+ def __init__(self, config):
404
+ super().__init__()
405
+ self.config = config
406
+ if self.config.use_vae:
407
+ # Трансформер-энкодер, который кодирует [cls, robot_state, action_sequence]
408
+ self.vae_encoder = ACTEncoder(config, is_vae_encoder=True)
409
+ # Эмбеддинг для токена CLS — из него извлекаются параметры латента
410
+ self.vae_encoder_cls_embed = nn.Embedding(1, config.dim_model)
411
+
412
+ # Проекция состояния робота в пространство модели
413
+ if self.config.robot_state_feature is not None:
414
+ self.vae_encoder_robot_state_input_proj = nn.Linear(
415
+ self.config.robot_state_feature.shape[0], config.dim_model
416
+ )
417
+
418
+ # Проекция целевых действий в пространство модели (для VAE-энкодера)
419
+ self.vae_encoder_action_input_proj = nn.Linear(
420
+ self.config.action_feature.shape[0],
421
+ config.dim_model,
422
+ )
423
+
424
+ # Проекция выхода VAE-энкодера в параметры латентного распределения
425
+ # (mean и log_var для каждой размерности латента)
426
+ self.vae_encoder_latent_output_proj = nn.Linear(
427
+ config.dim_model, config.latent_dim * 2
428
+ )
429
+
430
+ # Позиционные эмбеддинги для VAE-энкодера
431
+ num_input_token_encoder = 1 + config.chunk_size # 1 cls + chunk_size действий
432
+ if self.config.robot_state_feature is not None:
433
+ num_input_token_encoder += 1 # +1 для состояния робота
434
+
435
+ self.register_buffer(
436
+ "vae_encoder_pos_enc",
437
+ create_sinusoidal_pos_embedding(num_input_token_encoder, config.dim_model).unsqueeze(0),
438
+ persistent=False,
439
+ )
440
+
441
+ if self.config.image_features:
442
+ # Загружаем предобученную модель (например, ResNet)
443
+ backbone_model = getattr(torchvision.models, config.vision_backbone)(
444
+ replace_stride_with_dilation=[False, False, config.replace_final_stride_with_dilation],
445
+ weights=config.pretrained_backbone_weights,
446
+ norm_layer=FrozenBatchNorm2d,
447
+ )
448
+ # Получаем feature map из последнего блока (layer4)
449
+ self.backbone = IntermediateLayerGetter(backbone_model, return_layers={"layer4": "feature_map"})
450
+
451
+ # ---------------------------------------------------------------------
452
+ # Трансформер (основной энкодер и декодер)
453
+ # ---------------------------------------------------------------------
454
+ self.encoder = ACTEncoder(config)
455
+ self.decoder = ACTDecoder(config)
456
+
457
+ # Проекции входов в пространство трансформера
458
+ if self.config.robot_state_feature is not None:
459
+ self.encoder_robot_state_input_proj = nn.Linear(
460
+ self.config.robot_state_feature.shape[0], config.dim_model
461
+ )
462
+
463
+ if self.config.env_state_feature is not None:
464
+ self.encoder_env_state_input_proj = nn.Linear(
465
+ self.config.env_state_feature.shape[0], config.dim_model
466
+ )
467
+ # Проекция латентной переменной (z) в пространство модели
468
+ self.encoder_latent_input_proj = nn.Linear(config.latent_dim, config.dim_model)
469
+
470
+ if self.config.image_features:
471
+ # Проецируем каналы feature map в dim_model
472
+ # backbone_model.fc.in_features — это количество каналов layer4 у выбранной модели
473
+ self.encoder_img_feat_input_proj = nn.Conv2d(
474
+ backbone_model.fc.in_features, config.dim_model, kernel_size=1
475
+ )
476
+
477
+ # Позиционные эмбеддинги
478
+ n_1d_tokens = 1 # для латентной переменной
479
+ if self.config.robot_state_feature is not None:
480
+ n_1d_tokens += 1
481
+ if self.config.env_state_feature is not None:
482
+ n_1d_tokens += 1
483
+
484
+ self.encoder_1d_feature_pos_embed = nn.Embedding(n_1d_tokens, config.dim_model)
485
+
486
+ if self.config.image_features:
487
+ self.encoder_cam_feat_pos_embed = ACTSinusoidalPositionEmbedding2d(config.dim_model // 2)
488
+
489
+ # Learnable positional embeddings для декодера (как в DETR)
490
+ self.decoder_pos_embed = nn.Embedding(config.chunk_size, config.dim_model)
491
+
492
+ # Head для предсказания действий
493
+ self.action_head = nn.Linear(config.dim_model, self.config.action_feature.shape[0])
494
+
495
+ self._reset_parameters()
496
+
497
+ def _reset_parameters(self):
498
+ """Инициализация весов трансформера (Xavier uniform)."""
499
+ for p in chain(self.encoder.parameters(), self.decoder.parameters()):
500
+ if p.dim() > 1:
501
+ nn.init.xavier_uniform_(p)
502
+
503
+ def forward(
504
+ self,
505
+ batch: dict[str, Tensor]
506
+ ) -> tuple[Tensor, tuple[Tensor, Tensor] | tuple[None, None]]:
507
+
508
+ # ---------------------------------------------------------------------
509
+ # Шаг 1: VAE-энкодер — получаем латентную переменную z
510
+ # ---------------------------------------------------------------------
511
+ mu_hat = log_sigma_x2_hat = None
512
+ if self.config.use_vae and ACTION in batch:
513
+ # Собираем входные токены для VAE-энкодера: [cls, robot_state (если есть), action_sequence]
514
+ tokens = []
515
+ # Токен CLS
516
+ cls_token = self.vae_encoder_cls_embed(torch.zeros(batch[OBS_STATE].size(0), dtype=torch.long, device=batch[OBS_STATE].device))
517
+ tokens.append(cls_token)
518
+
519
+ # Состояние робота (если есть)
520
+ if self.config.robot_state_feature is not None:
521
+ robot_state_proj = self.vae_encoder_robot_state_input_proj(batch[OBS_STATE])
522
+ tokens.append(robot_state_proj)
523
+
524
+ # Целевые действия (для VAE)
525
+ action_proj = self.vae_encoder_action_input_proj(batch[ACTION])
526
+ # action_proj имеет форму (B, chunk_size, dim_model), разворачиваем в последовательность
527
+ tokens.append(action_proj.transpose(1, 0)) # (chunk_size, B, dim_model) -> потом сложим
528
+
529
+ # Объединяем токены в одну последовательность (B, seq_len, dim_model)
530
+ vae_input = torch.cat(tokens, dim=1) # (B, 1+state+chunk_size, dim_model)
531
+
532
+ # Добавляем позиционные эмбеддинги
533
+ vae_input += self.vae_encoder_pos_enc[:, :vae_input.size(1), :]
534
+
535
+ # Пропускаем через VAE-энкодер
536
+ vae_output = self.vae_encoder(vae_input) # (B, seq_len, dim_model)
537
+
538
+ # Берём только выход токена CLS (первый токен)
539
+ cls_output = vae_output[:, 0, :] # (B, dim_model)
540
+
541
+ # Преобразуем в параметры распределения латента
542
+ latent_params = self.vae_encoder_latent_output_proj(cls_output) # (B, latent_dim*2)
543
+ mu_hat, log_sigma_x2_hat = latent_params.chunk(2, dim=-1) # разделяем mean и log_var
544
+
545
+ # Сэмплируем латент z через трюк репараметризации
546
+ std = torch.exp(0.5 * log_sigma_x2_hat)
547
+ eps = torch.randn_like(std)
548
+ z = mu_hat + eps * std # (B, latent_dim)
549
+ else:
550
+ # Если VAE не используется (инференс или обучение без VAE), z = 0
551
+ batch_size = batch[OBS_STATE].dim()
552
+ z = torch.zeros(batch_size, self.config.latent_dim, device=batch[OBS_STATE].device)
553
+ # ---------------------------------------------------------------------
554
+ # Шаг 2: Извлечение признаков изображений (если есть)
555
+ # ---------------------------------------------------------------------
556
+ img_features = None
557
+ if self.config.image_features:
558
+ images = batch[OBS_IMAGES]
559
+ # images может быть списком камер или одним тензором (B, C, H, W) или (B, N, C, H, W)
560
+ # В LeRobot обычно это список тензоров (B, C, H, W) для каждой камеры
561
+ if isinstance(images, list):
562
+ # Для каждой камеры получаем feature map
563
+ img_feats = []
564
+ for img in images:
565
+ feat_dict = self.backbone(img)
566
+ img_feats.append(feat_dict["feature_map"]) # (B, C_feat, H_feat, W_feat)
567
+ # Склеиваем по каналам: (B, N*C_feat, H, W)
568
+ img_features = torch.cat(img_feats, dim=1)
569
+ else:
570
+ # Если у��е один тензор
571
+ feat_dict = self.backbone(images)
572
+ img_features = feat_dict["feature_map"]
573
+
574
+ # Проецируем в dim_model и добавляем 2D позиционные эмбеддинги
575
+ img_features = self.encoder_img_feat_input_proj(img_features) # (B, dim_model, H, W)
576
+ pos_embed_2d = self.encoder_cam_feat_pos_embed(img_features)
577
+ img_features += pos_embed_2d
578
+
579
+ # Разворачиваем feature map в последовательность токенов (B, H*W, dim_model)
580
+ B, C, H, W = img_features.shape
581
+ img_tokens = einops.rearrange(img_features, "b c h w -> b (h w) c")
582
+ else:
583
+ img_tokens = None
584
+
585
+ # ---------------------------------------------------------------------
586
+ # Шаг 3: Сбор токенов для основного трансформера-энкодера
587
+ # ---------------------------------------------------------------------
588
+ encoder_tokens = []
589
+
590
+ # Латент z
591
+ z_proj = self.encoder_latent_input_proj(z) # (B, dim_model)
592
+ z_proj_token = z_proj.unsqueeze(1) # (B, 1, D)
593
+ encoder_tokens.append(z_proj_token) # (B, 1, dim_model)
594
+
595
+ # Состояние робота
596
+ if self.config.robot_state_feature is not None:
597
+ robot_state = batch[OBS_STATE]
598
+ # если это один пример, делаем батч размера 1
599
+ robot_state = robot_state.unsqueeze(0)
600
+ robot_state_proj = self.encoder_robot_state_input_proj(robot_state) # (B, 512)
601
+ robot_state_proj_token = robot_state_proj.unsqueeze(1) # (B, 1, D)
602
+ encoder_tokens.append(robot_state_proj_token)
603
+ #print ("robot_state_proj_t: ", robot_state_proj_token.shape)
604
+
605
+ # Состояние среды
606
+ if self.config.env_state_feature:
607
+ env_state_proj = self.encoder_env_state_input_proj(batch[OBS_ENV_STATE]) # (B, dim_model)
608
+ env_state_proj_token = env_state_proj.unsqueeze(1) # (B, 1, D)
609
+ encoder_tokens.append(env_state_proj_token)
610
+ #print ("env_state_proj: ", env_state_proj_token.shape)
611
+
612
+ # Признаки изображений (если есть)
613
+ if img_tokens is not None:
614
+ encoder_tokens.append(img_tokens) # (B, H*W, dim_model)
615
+
616
+ for i, t in enumerate(encoder_tokens):
617
+ print(i, t.shape, t.dtype)
618
+
619
+
620
+ # Объединяем все токены
621
+ encoder_input = torch.cat(encoder_tokens, dim=1) # (B, num_tokens, dim_model)
622
+
623
+ # Количество 1D токенов = len(encoder_tokens) - (1 если есть img_tokens else 0)
624
+ num_1d_tokens = len(encoder_tokens)
625
+ if img_tokens is not None:
626
+ num_1d_tokens -= 1 # image tokens используют 2D pos embed, не 1D
627
+ encoder_input[:, :num_1d_tokens, :] += self.encoder_1d_feature_pos_embed.weight.unsqueeze(0)
628
+
629
+ # Пропускаем через энкодер трансформера
630
+ encoder_output = self.encoder(encoder_input) # (B, num_tokens, dim_model)
631
+
632
+ # Query для декодера — learnable pos embeddings длины chunk_size
633
+ decoder_queries = self.decoder_pos_embed.weight.unsqueeze(0).expand(batch_size, -1, -1)
634
+
635
+ # Декодер делает cross-attention к выходу энкодера
636
+ decoder_output = self.decoder(
637
+ decoder_queries,
638
+ encoder_output,
639
+ decoder_pos_embed=None, # уже в decoder_queries
640
+ encoder_pos_embed=None
641
+ ) # (B, chunk_size, dim_model)
642
+
643
+ actions_hat = self.action_head(decoder_output) # (B, chunk_size, action_dim)
644
+
645
+ return actions_hat, (mu_hat, log_sigma_x2_hat)
646
+
647
+
648
+ class ACTPolicy(nn.Module):
649
+
650
+ name = "act"
651
+
652
+ def __init__(
653
+ self,
654
+ config,
655
+ **kwargs,
656
+ ):
657
+
658
+ super().__init__()
659
+
660
+ config.validate_features()
661
+ self.config = config
662
+
663
+ # Инициализируем ядро модели ACT
664
+ self.model = ACT(config)
665
+
666
+ # Если включён temporal ensembling — создаём сглаживатель
667
+ if config.temporal_ensemble_coeff is not None:
668
+ self.temporal_ensembler = ACTTemporalEnsembler(
669
+ config.temporal_ensemble_coeff,
670
+ config.chunk_size
671
+ )
672
+ else:
673
+ # Иначе используем простую очередь действий (deque)
674
+ self._action_queue = deque([], maxlen=self.config.n_action_steps)
675
+
676
+ self.reset()
677
+
678
+ def get_optim_params(self) -> dict:
679
+ return [
680
+ {
681
+ "params": [
682
+ p
683
+ for n, p in self.named_parameters()
684
+ if not n.startswith("model.backbone") and p.requires_grad
685
+ ]
686
+ },
687
+ {
688
+ "params": [
689
+ p
690
+ for n, p in self.named_parameters()
691
+ if n.startswith("model.backbone") and p.requires_grad
692
+ ],
693
+ "lr": self.config.optimizer_lr_backbone,
694
+ },
695
+ ]
696
+
697
+ def reset(self):
698
+ if self.config.temporal_ensemble_coeff is not None:
699
+ self.temporal_ensembler.reset()
700
+ else:
701
+ # Очищаем очередь действий, если не используем temporal ensembling
702
+ self._action_queue.clear()
703
+
704
+ @torch.no_grad()
705
+ def select_action(self, batch: dict[str, Tensor]) -> Tensor:
706
+
707
+ batch_size = batch[OBS_STATE].size(0)
708
+
709
+ if self.config.temporal_ensemble_coeff is not None:
710
+ # Temporal ensembling: обновляем сглаженный прогноз и берём первое действие
711
+ actions_chunk, _ = self.model(batch) # (B, chunk_size, action_dim)
712
+ action = self.temporal_ensembler.update(actions_chunk)
713
+ return action
714
+
715
+ # Обычная логика с очередью (без ensembling)
716
+ if len(self._action_queue) == 0:
717
+ # Очередь пуста: делаем предсказание и кладём чанк целиком
718
+ actions_chunk, _ = self.model(batch) # (B, chunk_size, action_dim)
719
+ # Разворачиваем чанк по батчу и добавляем все действия в очередь
720
+ for t in range(actions_chunk.size(1)):
721
+ self._action_queue.append(actions_chunk[:, t, :])
722
+
723
+ # Берём первое действие из очереди
724
+ action = self._action_queue.popleft() # (B, action_dim)
725
+ return action
so101_scene.xml ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <mujoco model="so101_pick_place">
2
+ <compiler angle="radian" autolimits="true"/>
3
+
4
+ <option timestep="0.002" gravity="0 0 -9.81" integrator="Euler">
5
+ <flag contact="enable"/>
6
+ </option>
7
+
8
+ <default>
9
+ <joint armature="0.02" damping="1.5"/>
10
+ <geom condim="4" friction="1.0 0.005 0.001" solimp="0.95 0.99 0.001 0.5 2" solref="0.02 1.0"/>
11
+ <position kp="100" ctrlrange="-3.14159 3.14159"/>
12
+ </default>
13
+
14
+ <visual>
15
+ <global offwidth="960" offheight="720"/>
16
+ <quality shadowsize="2048"/>
17
+ <map znear="0.01" zfar="10"/>
18
+ </visual>
19
+
20
+ <!-- ==================== ASSETS ==================== -->
21
+ <asset>
22
+ <texture name="grid" type="2d" builtin="checker" rgb1="0.85 0.85 0.85" rgb2="0.7 0.7 0.7" width="256" height="256"/>
23
+ <material name="floor_mat" texture="grid" texrepeat="8 8" reflectance="0.1"/>
24
+ <texture name="wood" type="2d" builtin="flat" rgb1="0.65 0.45 0.25" rgb2="0.55 0.35 0.18" width="256" height="256" mark="random" markrgb="0.6 0.4 0.2"/>
25
+ <material name="wood_mat" texture="wood" reflectance="0.05"/>
26
+ <material name="arm_mat" rgba="0.25 0.25 0.28 1" specular="0.5" shininess="0.5"/>
27
+ <material name="finger_mat" rgba="0.15 0.15 0.15 1" specular="0.3"/>
28
+ <material name="box_mat" rgba="0.85 0.15 0.15 1" specular="0.3" shininess="0.3"/>
29
+ <material name="bowl_mat" rgba="0.2 0.4 0.85 1" specular="0.4" shininess="0.5"/>
30
+ <material name="base_mat" rgba="0.3 0.3 0.35 1" specular="0.5" shininess="0.5"/>
31
+ </asset>
32
+
33
+ <!-- ==================== WORLD ==================== -->
34
+ <worldbody>
35
+ <!-- Floor -->
36
+ <geom name="floor" type="plane" size="1 1 0.01" pos="0 0 0" material="floor_mat" contype="1" conaffinity="1"/>
37
+
38
+ <!-- Ambient and directional lights -->
39
+ <light name="overhead" directional="true" diffuse="0.8 0.8 0.8" specular="0.3 0.3 0.3" pos="0 0 2" dir="0 0 -1" castshadow="true"/>
40
+ <light name="side_fill" directional="false" diffuse="0.4 0.4 0.4" specular="0.1 0.1 0.1" pos="0.5 -0.5 1.2"/>
41
+ <light name="back_fill" directional="false" diffuse="0.2 0.2 0.2" specular="0.05 0.05 0.05" pos="-0.3 0.4 1.0"/>
42
+
43
+ <!-- Overhead camera -->
44
+ <!-- Overhead camera — full view of table, arm, and objects -->
45
+ <camera name="webcam" pos="0.0 -0.5 1.1" quat="0.92 0.39 0 0" fovy="60"/>
46
+
47
+ <!-- ==================== TABLE ==================== -->
48
+ <body name="table" pos="0 0 0.2">
49
+ <geom name="table_top" type="box" size="0.30 0.25 0.02" pos="0 0 0.20" material="wood_mat" contype="1" conaffinity="1" mass="10"/>
50
+ <!-- Table legs -->
51
+ <geom type="cylinder" size="0.02 0.10" pos="0.25 0.20 0.10" material="wood_mat"/>
52
+ <geom type="cylinder" size="0.02 0.10" pos="-0.25 0.20 0.10" material="wood_mat"/>
53
+ <geom type="cylinder" size="0.02 0.10" pos="0.25 -0.20 0.10" material="wood_mat"/>
54
+ <geom type="cylinder" size="0.02 0.10" pos="-0.25 -0.20 0.10" material="wood_mat"/>
55
+ </body>
56
+
57
+ <!-- ==================== ROBOT ARM ==================== -->
58
+ <body name="base_link" pos="0 -0.15 0.42">
59
+ <geom name="base" type="cylinder" size="0.04 0.02" material="base_mat" mass="1.0"/>
60
+
61
+ <!-- Joint 1: Shoulder Pan (yaw) -->
62
+ <body name="shoulder_link" pos="0 0 0.03">
63
+ <joint name="shoulder_pan" type="hinge" axis="0 0 1" range="-3.14159 3.14159"/>
64
+ <geom name="shoulder_geom" type="capsule" fromto="0 0 0 0 0 0.06" size="0.025" material="arm_mat" mass="0.3"/>
65
+
66
+ <!-- Joint 2: Shoulder Lift (pitch) -->
67
+ <body name="upper_arm_link" pos="0 0 0.06">
68
+ <joint name="shoulder_lift" type="hinge" axis="0 1 0" range="-1.5708 1.5708"/>
69
+ <geom name="upper_arm_geom" type="capsule" fromto="0 0 0 0.10 0 0.04" size="0.02" material="arm_mat" mass="0.2"/>
70
+
71
+ <!-- Joint 3: Elbow Flex (pitch) -->
72
+ <body name="forearm_link" pos="0.10 0 0.04">
73
+ <joint name="elbow_flex" type="hinge" axis="0 1 0" range="-3.14159 0"/>
74
+ <geom name="forearm_geom" type="capsule" fromto="0 0 0 0.10 0 -0.02" size="0.018" material="arm_mat" mass="0.15"/>
75
+
76
+ <!-- Joint 4: Wrist Flex (pitch) -->
77
+ <body name="wrist_flex_link" pos="0.10 0 -0.02">
78
+ <joint name="wrist_flex" type="hinge" axis="0 1 0" range="-1.5708 1.5708"/>
79
+ <geom name="wrist_flex_geom" type="capsule" fromto="0 0 0 0.04 0 0" size="0.015" material="arm_mat" mass="0.1"/>
80
+
81
+ <!-- Joint 5: Wrist Roll (roll) -->
82
+ <body name="wrist_roll_link" pos="0.04 0 0">
83
+ <joint name="wrist_roll" type="hinge" axis="1 0 0" range="-3.14159 3.14159"/>
84
+ <geom name="wrist_roll_geom" type="cylinder" size="0.016 0.01" material="arm_mat" mass="0.05"/>
85
+
86
+ <!-- Wrist-mounted camera -->
87
+ <!-- Wrist camera: mounted above gripper, looking down at the fingers/workspace -->
88
+ <!-- Wrist camera: looks along -local_z toward the gripper/workspace -->
89
+ <!-- Wrist camera: looks down toward gripper/workspace -->
90
+ <!-- Wrist camera: mounted on gripper mount, looking forward/down through the fingers -->
91
+ <camera name="arm_cam" pos="0 0 -0.025" xyaxes="0 1 0 0 0 1" fovy="90"/>
92
+
93
+ <!-- End-effector site (at grasp center between finger pads) -->
94
+ <!-- EE site at center of finger pads (invisible — for IK only) -->
95
+ <!-- EE site at grasp center (invisible) -->
96
+ <site name="ee_site" pos="0.0 0 -0.046" size="0.001" rgba="0 0 0 0"/>
97
+ <!-- finger pads center at body(-0.024) + geom(0, +-0.006, -0.022) = wrist + (0, +-0.006, -0.046) -->
98
+
99
+ <!-- ==================== GRIPPER ==================== -->
100
+ <!-- Parallel jaw gripper. Fixed mount plate at wrist, two sliding
101
+ finger prongs. qpos=0 -> closed, qpos=0.04 -> open. -->
102
+
103
+ <!-- Fixed gripper mount plate (always attached to wrist) -->
104
+ <geom name="gripper_mount" type="box" size="0.012 0.028 0.006" pos="0 0 -0.018"
105
+ material="finger_mat" mass="0.02"/>
106
+
107
+ <!-- Left finger: slides along +Y -->
108
+ <body name="left_finger" pos="0.0 0.0 -0.024">
109
+ <joint name="finger_left" type="slide" axis="0 1 0" range="0 0.04" damping="0.3"/>
110
+ <!-- Horizontal slider bar (stays connected to mount visually) -->
111
+ <geom name="left_finger_slider" type="box" size="0.006 0.018 0.004" pos="0 0.008 0"
112
+ material="finger_mat" mass="0.003" contype="0" conaffinity="0"/>
113
+ <!-- Vertical finger pad (gripping surface) -->
114
+ <geom name="left_finger_pad" type="box" size="0.008 0.005 0.022" pos="0 0.006 -0.022"
115
+ material="finger_mat" mass="0.01"
116
+ condim="4" friction="5.0 0.01 0.001"
117
+ solimp="0.95 0.99 0.001 0.5 2" solref="0.02 1.0"/>
118
+ </body>
119
+
120
+ <!-- Right finger: slides along -Y -->
121
+ <body name="right_finger" pos="0.0 0.0 -0.024">
122
+ <joint name="finger_right" type="slide" axis="0 -1 0" range="0 0.04" damping="0.3"/>
123
+ <!-- Horizontal slider bar -->
124
+ <geom name="right_finger_slider" type="box" size="0.006 0.018 0.004" pos="0 -0.008 0"
125
+ material="finger_mat" mass="0.003" contype="0" conaffinity="0"/>
126
+ <!-- Vertical finger pad -->
127
+ <geom name="right_finger_pad" type="box" size="0.008 0.005 0.022" pos="0 -0.006 -0.022"
128
+ material="finger_mat" mass="0.01"
129
+ condim="4" friction="5.0 0.01 0.001"
130
+ solimp="0.95 0.99 0.001 0.5 2" solref="0.02 1.0"/>
131
+ </body>
132
+
133
+ </body> <!-- wrist_roll_link -->
134
+ </body> <!-- wrist_flex_link -->
135
+ </body> <!-- forearm_link -->
136
+ </body> <!-- upper_arm_link -->
137
+ </body> <!-- shoulder_link -->
138
+ </body> <!-- base_link -->
139
+
140
+ <!-- ==================== OBJECTS ==================== -->
141
+
142
+ <!-- Graspable box -->
143
+ <body name="box" pos="0.15 0.0 0.44">
144
+ <freejoint name="box_joint"/>
145
+ <geom name="box_geom" type="box" size="0.015 0.015 0.015"
146
+ material="box_mat" mass="0.03"
147
+ condim="4" friction="1.5 0.005 0.001"
148
+ solimp="0.95 0.99 0.001 0.5 2" solref="0.02 1.0"
149
+ contype="1" conaffinity="1"/>
150
+ </body>
151
+
152
+ <!-- Bowl (static) -->
153
+ <body name="bowl" pos="-0.15 0.0 0.42">
154
+ <!-- Bowl base (thin disc) -->
155
+ <geom name="bowl_base" type="cylinder" size="0.045 0.003" pos="0 0 0" material="bowl_mat" contype="1" conaffinity="1"/>
156
+ <!-- Bowl walls (ring of thin boxes approximating a cylinder shell) -->
157
+ <geom name="bowl_wall_front" type="box" size="0.003 0.045 0.02" pos="0.045 0 0.02" material="bowl_mat" contype="1" conaffinity="1"/>
158
+ <geom name="bowl_wall_back" type="box" size="0.003 0.045 0.02" pos="-0.045 0 0.02" material="bowl_mat" contype="1" conaffinity="1"/>
159
+ <geom name="bowl_wall_left" type="box" size="0.045 0.003 0.02" pos="0 0.045 0.02" material="bowl_mat" contype="1" conaffinity="1"/>
160
+ <geom name="bowl_wall_right" type="box" size="0.045 0.003 0.02" pos="0 -0.045 0.02" material="bowl_mat" contype="1" conaffinity="1"/>
161
+ <!-- Diagonal walls for rounder shape -->
162
+ <geom name="bowl_wall_fl" type="box" size="0.003 0.045 0.02" pos="0.032 0.032 0.02" euler="0 0 0.785" material="bowl_mat" contype="1" conaffinity="1"/>
163
+ <geom name="bowl_wall_fr" type="box" size="0.003 0.045 0.02" pos="0.032 -0.032 0.02" euler="0 0 -0.785" material="bowl_mat" contype="1" conaffinity="1"/>
164
+ <geom name="bowl_wall_bl" type="box" size="0.003 0.045 0.02" pos="-0.032 0.032 0.02" euler="0 0 -0.785" material="bowl_mat" contype="1" conaffinity="1"/>
165
+ <geom name="bowl_wall_br" type="box" size="0.003 0.045 0.02" pos="-0.032 -0.032 0.02" euler="0 0 0.785" material="bowl_mat" contype="1" conaffinity="1"/>
166
+ </body>
167
+
168
+ </worldbody>
169
+
170
+ <!-- ==================== ACTUATORS ==================== -->
171
+ <actuator>
172
+ <position name="act_shoulder_pan" joint="shoulder_pan" kp="80" ctrlrange="-3.14159 3.14159"/>
173
+ <position name="act_shoulder_lift" joint="shoulder_lift" kp="80" ctrlrange="-1.5708 1.5708"/>
174
+ <position name="act_elbow_flex" joint="elbow_flex" kp="80" ctrlrange="-3.14159 0"/>
175
+ <position name="act_wrist_flex" joint="wrist_flex" kp="60" ctrlrange="-1.5708 1.5708"/>
176
+ <position name="act_wrist_roll" joint="wrist_roll" kp="40" ctrlrange="-3.14159 3.14159"/>
177
+ <position name="act_finger_left" joint="finger_left" kp="15" ctrlrange="0 0.04"/>
178
+ <position name="act_finger_right" joint="finger_right" kp="15" ctrlrange="0 0.04"/>
179
+ </actuator>
180
+
181
+ <equality>
182
+ <!-- Couple gripper fingers to move symmetrically -->
183
+ <joint joint1="finger_left" joint2="finger_right" polycoef="0 1 0 0 0"/>
184
+ </equality>
185
+
186
+ <!-- ==================== SENSORS ==================== -->
187
+ <sensor>
188
+ <jointpos name="sen_shoulder_pan" joint="shoulder_pan"/>
189
+ <jointpos name="sen_shoulder_lift" joint="shoulder_lift"/>
190
+ <jointpos name="sen_elbow_flex" joint="elbow_flex"/>
191
+ <jointpos name="sen_wrist_flex" joint="wrist_flex"/>
192
+ <jointpos name="sen_wrist_roll" joint="wrist_roll"/>
193
+ <jointpos name="sen_finger_left" joint="finger_left"/>
194
+ <framepos name="sen_ee_pos" objtype="site" objname="ee_site"/>
195
+ <framequat name="sen_ee_quat" objtype="site" objname="ee_site"/>
196
+ </sensor>
197
+
198
+ </mujoco>