szcharlesji's picture
Add files using upload-large-folder tool
a3f6fc6 verified
Raw History Blame Contribute Delete
6.03 kB
"""YOR-only waypoint filtering and lossless interleaved temporal subsampling."""
import numpy as np
from datasets.core import TrajectoryDataset
DEFAULT_THRESHOLDS = dict(left_xyz_m=.005,right_xyz_m=.005,left_rotation_deg=3.,
right_rotation_deg=3.,left_hand_rad=.08,right_hand_rad=.08,lift_m=.005,
base_xy_m=.01,base_yaw_rad=.03)
GROUPS = dict(left_xyz_m=slice(4,7),right_xyz_m=slice(11,14),
left_hand_rad=slice(14,30),right_hand_rad=slice(34,50),lift_m=slice(54,55),
base_xy_m=slice(55,57),base_yaw_rad=slice(57,58))
def validate_thresholds(thresholds):
unknown=set(thresholds)-set(DEFAULT_THRESHOLDS)
if unknown: raise ValueError(f'Unknown motion thresholds: {unknown}')
for key,t in thresholds.items():
if t is None: continue
t=np.asarray(t,float)
width=GROUPS[key].stop-GROUPS[key].start if key in GROUPS else 1
if t.ndim>1 or t.size not in (1,width) or not np.isfinite(t).all() or (t<=0).any():
raise ValueError(f'{key}: use null to disable, or positive scalar/{width}-vector')
def motion_scores(actions,reference,thresholds):
"""Largest per-axis normalized change. Quaternion change is sign-invariant."""
a=np.asarray(actions,float);b=np.asarray(reference,float);scores=[]
for key,t in thresholds.items():
if t is None: continue
if key in GROUPS:
d=np.abs(a[...,GROUPS[key]]-b[GROUPS[key]])
if key=='base_yaw_rad':d=np.abs((a[...,57:58]-b[57:58]+np.pi)%(2*np.pi)-np.pi)
scores.append(np.max(d/np.asarray(t),axis=-1))
else:
sl=slice(0,4) if key=='left_rotation_deg' else slice(7,11)
q=a[...,sl];r=b[sl];den=np.linalg.norm(q,axis=-1)*np.linalg.norm(r)
if (den<1e-9).any():raise ValueError('Invalid quaternion in motion selection')
angle=np.degrees(2*np.arccos(np.clip(np.abs(np.sum(q*r,axis=-1))/den,0,1)))
scores.append(angle/float(np.asarray(t).reshape(-1)[0]))
return np.max(np.stack(scores),axis=0) if scores else np.zeros(a.shape[:-1])
def select_waypoints(actions,enabled=False,thresholds=None,max_gap_frames=30):
"""Keep first/last, threshold crossings vs last kept, and optional hold anchors.
max_gap_frames=0 removes the hold-anchor rule. It does not enforce minimum
motion on anchors or endpoints. Source data and source indices are unchanged.
"""
a=np.asarray(actions);n=len(a)
if a.ndim!=2 or a.shape[1]!=58 or not np.isfinite(a).all():raise ValueError('Expected finite [T,58] actions')
if int(max_gap_frames)!=max_gap_frames or max_gap_frames<0:raise ValueError('max_gap_frames must be a nonnegative integer')
t=dict(DEFAULT_THRESHOLDS if thresholds is None else thresholds);validate_thresholds(t)
if not enabled or n<2:return np.arange(n,dtype=np.int64)
kept=[0]
for i in range(1,n):
if motion_scores(a[i],a[kept[-1]],t)>=1 or (max_gap_frames and i-kept[-1]>=max_gap_frames):kept.append(i)
if kept[-1]!=n-1:kept.append(n-1)
return np.asarray(kept,dtype=np.int64)
def phase_indices(indices,frame_skip):
if int(frame_skip)!=frame_skip or frame_skip<1:raise ValueError('frame_skip must be a positive integer')
return [np.asarray(indices)[p::int(frame_skip)] for p in range(min(int(frame_skip),len(indices)))]
class YorTemporalDataset(TrajectoryDataset):
"""Wrap AFTER episode splitting and embedding; phases cannot leak across splits."""
def __init__(self,dataset,frame_skip=1,motion_filter=None):
self.dataset=dataset;self.frame_skip=frame_skip;self.motion_filter=dict(motion_filter or {})
self.sequences=[]
for i in range(len(dataset)):
a=dataset.get_frames(i,range(dataset.get_seq_length(i)))[1]
if hasattr(a,'detach'):a=a.detach().cpu().numpy()
ids=select_waypoints(a,**self.motion_filter)
self.sequences.extend((i,phase,frames) for phase,frames in enumerate(phase_indices(ids,frame_skip)))
def __len__(self):return len(self.sequences)
def get_seq_length(self,idx):return len(self.sequences[idx][2])
def get_frames(self,idx,frames):
i,phase,ids=self.sequences[idx]
return self.dataset.get_frames(i,ids[np.asarray(list(frames),dtype=int)].tolist())
def __getitem__(self,idx):return self.get_frames(idx,range(self.get_seq_length(idx)))
def get_all_actions(self):
import torch
return torch.cat([self[i][1] for i in range(len(self))])
from datasets.core import TrajectorySlicerDataset
class YorTemporalSlicer(TrajectorySlicerDataset):
"""Pad short phases and optionally predict actions after each observation.
action_offset is measured in sampled sequence rows. With offset 1 the
current observation at t predicts t+1 onward. Exclude anchors that have no
next target, but repeat the last target when a chunk reaches the end.
"""
def __init__(self,dataset,action_offset=0,**kwargs):
if int(action_offset)!=action_offset or action_offset<0:
raise ValueError('action_offset must be a nonnegative integer')
self.action_offset=int(action_offset)
super().__init__(dataset,**kwargs)
self.slices=[(i,t) for i in range(len(dataset))
for t in range(max(0,dataset.get_seq_length(i)-self.action_offset))
if self.pad_seq_length or t+self.action_offset+self.action_window<=dataset.get_seq_length(i)]
def source_indices(self,idx):
i,t=self.slices[idx];T=self.dataset.get_seq_length(i)
obs=np.clip(np.arange(t-self.window+1,t+1),0,T-1)
start=t if self.vqbet_get_future_action_chunk else t-self.window+1
act=np.clip(np.arange(start,t+self.action_window)+self.action_offset,self.action_offset,T-1)
return i,obs,act
def __getitem__(self,idx):
i,oi,ai=self.source_indices(idx)
obs,act,*others=self.dataset[i]
values=(obs[oi],act[ai],*[other[oi] for other in others])
return self.transform(values) if self.transform else values