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
2 changes: 1 addition & 1 deletion docs/source/overview/sim/solvers/srs_solver.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ cfg = SRSSolverCfg(
end_link_name="left_ee",
root_link_name="left_arm_base",
dh_params=arm_params.dh_params,
qpos_limits=arm_params.qpos_limits,
user_qpos_limit=arm_params.qpos_limits,
T_e_oe=arm_params.T_e_oe,
T_b_ob=arm_params.T_b_ob,
link_lengths=arm_params.link_lengths,
Expand Down
2 changes: 1 addition & 1 deletion docs/source/tutorial/solver.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ API Reference
"""Compute the Jacobian matrix for the given joint positions."""

- **set_ik_nearst_weight**: Set weights for IK nearest neighbor search.
- **set_position_limits / get_position_limits**: Set or get joint position limits.
- **set_qpos_limits / get_qpos_limits**: Set or get joint position limits.
- **set_tcp / get_tcp**: Set or get the tool center point (TCP) transformation.

Configuration
Expand Down
3 changes: 3 additions & 0 deletions embodichain/lab/sim/objects/robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,9 @@ def init_solver(self, cfg: Union[SolverCfg, Dict[str, SolverCfg]]) -> None:
):
solver_cfg.joint_names = self.cfg.control_parts[part_name]
self._solvers[name] = solver_cfg.init_solver(device=self.device)
joint_ids = self.get_joint_ids(name=part_name)
joint_limits = self._data.qpos_limits[0][joint_ids]
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self._data does not appear to be defined on Robot (this class uses self.body_data elsewhere). This will raise AttributeError during solver initialization. Use self.body_data.qpos_limits (or self.get_qpos_limits(...)) to fetch joint limits instead.

Suggested change
joint_limits = self._data.qpos_limits[0][joint_ids]
joint_limits = self.body_data.qpos_limits[0][joint_ids]

Copilot uses AI. Check for mistakes.
self._solvers[name].update_with_robot_limit(joint_limits)
Comment thread
matafela marked this conversation as resolved.

def get_solver(self, name: str | None = None) -> BaseSolver | None:
"""Get the kinematic solver for a specific control part.
Expand Down
2 changes: 2 additions & 0 deletions embodichain/lab/sim/robots/cobotmagic.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,15 @@ def _build_default_cfgs() -> Dict[str, Any]:
tcp=np.array(
[[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 1, 0.143], [0, 0, 0, 1]]
),
safe_margin=5.0 * np.pi / 180.0,
),
"right_arm": OPWSolverCfg(
end_link_name="right_link6",
root_link_name="right_arm_base",
tcp=np.array(
[[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 1, 0.143], [0, 0, 0, 1]]
),
safe_margin=5.0 * np.pi / 180.0,
),
},
"min_position_iters": 8,
Expand Down
4 changes: 2 additions & 2 deletions embodichain/lab/sim/robots/dexforce_w1/cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def _build_default_solver_cfg(is_industrial: bool) -> SolverCfg:
end_link_name="right_ee",
root_link_name="right_arm_base",
dh_params=w1_right_arm_params.dh_params,
qpos_limits=w1_right_arm_params.qpos_limits,
user_qpos_limits=w1_right_arm_params.qpos_limits,
T_e_oe=w1_right_arm_params.T_e_oe,
T_b_ob=w1_right_arm_params.T_b_ob,
link_lengths=w1_right_arm_params.link_lengths,
Expand All @@ -170,7 +170,7 @@ def _build_default_solver_cfg(is_industrial: bool) -> SolverCfg:
end_link_name="left_ee",
root_link_name="left_arm_base",
dh_params=w1_left_arm_params.dh_params,
qpos_limits=w1_left_arm_params.qpos_limits,
user_qpos_limits=w1_left_arm_params.qpos_limits,
T_e_oe=w1_left_arm_params.T_e_oe,
T_b_ob=w1_left_arm_params.T_b_ob,
link_lengths=w1_left_arm_params.link_lengths,
Expand Down
104 changes: 84 additions & 20 deletions embodichain/lab/sim/solvers/base_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ class SolverCfg:
when multiple solutions are available.
"""

user_qpos_limits: List[float] | None = None
"""
User defined Joint position limits [2, DOF] for the solver.
If not provided (None), this value will replace by joint limits defined in urdf when solver init from robot.
If provided, the solver will use the intersection of user defined limits and urdf limits as the final joint limits.
Comment on lines +75 to +79
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user_qpos_limits is described/consumed as a 2D limit matrix ([2, DOF] or [DOF, 2]), but the type annotation is List[float] | None (1D). Update the annotation to a 2D type (e.g., Sequence[Sequence[float]] | torch.Tensor | np.ndarray | None) to reflect the accepted shapes and catch misuse earlier.

Copilot uses AI. Check for mistakes.
"""

@abstractmethod
def init_solver(self, device: torch.device, **kwargs) -> "BaseSolver":
pass
Expand Down Expand Up @@ -165,6 +172,8 @@ def __init__(self, cfg: SolverCfg = None, device: str = None, **kwargs):
device=self.device,
)

self._init_qpos_limits()

def set_ik_nearest_weight(
self, ik_weight: np.ndarray, joint_ids: np.ndarray | None = None
) -> bool:
Expand Down Expand Up @@ -223,51 +232,106 @@ def get_ik_nearest_weight(self):
"""
return self.ik_nearest_weight

def set_position_limits(
def _init_qpos_limits(self):
self.lower_qpos_limits = None
self.upper_qpos_limits = None
if self.cfg.user_qpos_limits is not None:
# robot qpos limits from config, expected shape [DOF, 2]
user_qpos_limits = torch.tensor(
self.cfg.user_qpos_limits, dtype=torch.float32, device=self.device
)
if user_qpos_limits.shape == (2, self.dof):
self.set_qpos_limits(
lower_qpos_limits=user_qpos_limits[0],
upper_qpos_limits=user_qpos_limits[1],
)
elif user_qpos_limits.shape == (self.dof, 2):
self.set_qpos_limits(
lower_qpos_limits=user_qpos_limits[:, 0],
upper_qpos_limits=user_qpos_limits[:, 1],
)
else:
logger.log_error(
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When user_qpos_limits has an invalid shape, the code only logs an error and leaves lower_qpos_limits/upper_qpos_limits as None. Downstream code (e.g., OPW warp IK) indexes these tensors and will crash with a NoneType error. Consider raising a ValueError (or falling back to URDF limits) and returning early so the solver is never left in a partially-initialized state.

Suggested change
logger.log_error(
raise ValueError(

Copilot uses AI. Check for mistakes.
f"user_qpos_limits must have shape (2, {self.dof}) or ({self.dof}, 2), but got {user_qpos_limits.shape}."
)
elif self.pk_serial_chain is not None:
self.set_qpos_limits(
lower_qpos_limits=self.pk_serial_chain.low,
upper_qpos_limits=self.pk_serial_chain.high,
)

def update_with_robot_limit(self, robot_qpos_limits: torch.Tensor):
"""Update with robot joint limits.
Make sure the solver's joint limits are within the robot's joint limits.

Args:
robot_qpos_limits (torch.Tensor): [DOF, 2] tensor of joint limits from the robot data
"""
robot_lower_limits = robot_qpos_limits[:, 0]
robot_upper_limits = robot_qpos_limits[:, 1]
if self.lower_qpos_limits is not None:
if torch.any(self.lower_qpos_limits < robot_lower_limits):
logger.log_warning(
"Solver lower_qpos_limits are smaller than robot limits. Clamping to robot limits."
)
self.lower_qpos_limits = torch.max(
Comment on lines +273 to +277
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_with_robot_limit() clamps lower_qpos_limits and upper_qpos_limits independently, but doesn’t re-check that the resulting limits still satisfy lower <= upper. If user limits are inconsistent with robot limits, this can produce an impossible interval that later IK code can’t satisfy. Add a final validation step (and error/fallback) after clamping.

Copilot uses AI. Check for mistakes.
self.lower_qpos_limits, robot_lower_limits
)
else:
self.lower_qpos_limits = robot_lower_limits
if self.upper_qpos_limits is not None:
if torch.any(self.upper_qpos_limits > robot_upper_limits):
logger.log_warning(
"Solver upper_qpos_limits are larger than robot limits. Clamping to robot limits."
)
self.upper_qpos_limits = torch.min(
self.upper_qpos_limits, robot_upper_limits
)
else:
self.upper_qpos_limits = robot_upper_limits

def set_qpos_limits(
self,
lower_position_limits: List[float],
upper_position_limits: List[float],
lower_qpos_limits: List[float],
upper_qpos_limits: List[float],
) -> bool:
Comment on lines +293 to 297
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_qpos_limits() doesn’t validate that the provided limit arrays have length self.dof. If a caller passes a mismatched length, the solver can end up with incorrect limit tensors and later indexing/clamping bugs. Add an explicit length/shape check (and fail fast) before storing the tensors.

Copilot uses AI. Check for mistakes.
r"""Sets the upper and lower joint position limits.

Parameters:
lower_position_limits (List[float]): A list of lower limits for each joint.
upper_position_limits (List[float]): A list of upper limits for each joint.
lower_qpos_limits (List[float]): A list of lower limits for each joint.
upper_qpos_limits (List[float]): A list of upper limits for each joint.

Returns:
bool: True if limits are successfully set, False if the input is invalid.
"""
if (
len(lower_position_limits) != self.model.nq
or len(upper_position_limits) != self.model.nq
):
logger.log_warning("Length of limits must match the number of joints.")
return False

if any(
lower > upper
for lower, upper in zip(lower_position_limits, upper_position_limits)
lower > upper for lower, upper in zip(lower_qpos_limits, upper_qpos_limits)
):
Comment on lines +293 to 310
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_qpos_limits() no longer validates that the provided limits match the solver DOF. If the lengths differ, zip() will silently truncate and later indexing (e.g., in OPW kernels) can go out-of-bounds or use wrong limits. Add a length/shape check against self.dof (and return False / raise) before storing the limits.

Copilot uses AI. Check for mistakes.
logger.log_warning(
"Each lower limit must be less than or equal to the corresponding upper limit."
)
return False

self.lower_position_limits = np.array(lower_position_limits)
self.upper_position_limits = np.array(upper_position_limits)
self.lower_qpos_limits = torch.tensor(
lower_qpos_limits, dtype=float, device=self.device
)
self.upper_qpos_limits = torch.tensor(
upper_qpos_limits, dtype=float, device=self.device
)
return True

def get_position_limits(self) -> dict:
def get_qpos_limits(self) -> dict:
r"""Returns the current joint position limits.

Returns:
dict: A dictionary containing:
- lower_position_limits (List[float]): The current lower limits for each joint.
- upper_position_limits (List[float]): The current upper limits for each joint.
- lower_qpos_limits (List[float]): The current lower limits for each joint.
- upper_qpos_limits (List[float]): The current upper limits for each joint.
"""
return {
"lower_position_limits": self.lower_position_limits.tolist(),
"upper_position_limits": self.upper_position_limits.tolist(),
"lower_qpos_limits": self.lower_qpos_limits.tolist(),
"upper_qpos_limits": self.upper_qpos_limits.tolist(),
}

def set_tcp(self, xpos: np.ndarray):
Expand Down
88 changes: 67 additions & 21 deletions embodichain/lab/sim/solvers/opw_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
OPWparam,
opw_fk_kernel,
opw_ik_kernel,
opw_best_ik_kernel,
opw_ik_select_kernel,
wp_vec6f,
)
from embodichain.utils.device_utils import standardize_device_string
Expand Down Expand Up @@ -72,6 +72,9 @@ class OPWSolverCfg(SolverCfg):
# Parameters for the inverse-kinematics method.
ik_params: dict | None = None

# safe margin for joint limits, in radians
safe_margin: float = 5.0 * np.pi / 180.0

def init_solver(
self, device: torch.device = torch.device("cpu"), **kwargs
) -> "OPWSolver":
Expand Down Expand Up @@ -247,50 +250,87 @@ def get_ik_warp(
N_SOL = 8
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description/checklist indicates documentation and tests were updated/added, but this PR only changes opw_solver.py and doesn’t modify docs or tests. If tests/docs exist in separate commits/files, please include them here; otherwise update the PR description/checklist to match what’s actually changed.

Copilot uses AI. Check for mistakes.
DOF = 6
n_sample = target_xpos.shape[0]
kernel_device = standardize_device_string(self.device)

if target_xpos.shape == (4, 4):
target_xpos_batch = target_xpos[None, :, :]
target_xpos_batch = target_xpos[None, :, :].to(kernel_device)
else:
target_xpos_batch = target_xpos
target_xpos_batch = target_xpos.to(kernel_device)
Comment on lines 250 to +258
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n_sample is computed from target_xpos.shape[0] before the (4, 4) special-case, which makes n_sample==4 for a single pose and causes allocation/launch dimensions to mismatch target_xpos_batch (which becomes (1,4,4)). Set n_sample after batching (e.g., from target_xpos_batch.shape[0]) or explicitly set it to 1 in the (4,4) branch.

Copilot uses AI. Check for mistakes.
Comment on lines 250 to +258
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n_sample is computed before the (4, 4) special-case handling. If target_xpos is a single pose with shape (4, 4), n_sample becomes 4 (not 1), leading to incorrect buffer sizes and likely out-of-bounds indexing in the kernels. Move the n_sample computation to after target_xpos_batch is created (or explicitly set n_sample = 1 for the (4, 4) case).

Copilot uses AI. Check for mistakes.
target_xpos_wp = wp.from_torch(target_xpos_batch.reshape(-1))

all_qpos_wp = wp.zeros(
n_sample * N_SOL * DOF,
dtype=float,
device=standardize_device_string(self.device),
device=standardize_device_string(kernel_device),
)
all_ik_valid_wp = wp.zeros(
n_sample * N_SOL, dtype=int, device=standardize_device_string(self.device)
n_sample * N_SOL, dtype=int, device=standardize_device_string(kernel_device)
)

# TODO: whether require gradient
offsets_ = self.offsets.to(standardize_device_string(kernel_device))
sign_corrections_ = self.sign_corrections.to(
Comment on lines 270 to +272
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

offsets_ = self.offsets.to(...) and sign_corrections_ = ... will potentially allocate/copy Warp arrays on every IK call. If this function is called at high frequency, consider caching device-specific copies (or only copying when kernel_device differs) to reduce per-call overhead.

Copilot uses AI. Check for mistakes.
standardize_device_string(kernel_device)
)
lower_limits_ = wp_vec6f(
self.lower_qpos_limits[0],
self.lower_qpos_limits[1],
self.lower_qpos_limits[2],
self.lower_qpos_limits[3],
self.lower_qpos_limits[4],
self.lower_qpos_limits[5],
)
upper_limits_ = wp_vec6f(
self.upper_qpos_limits[0],
self.upper_qpos_limits[1],
self.upper_qpos_limits[2],
self.upper_qpos_limits[3],
self.upper_qpos_limits[4],
self.upper_qpos_limits[5],
Comment on lines +275 to +289
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lower_limits_/upper_limits_ are constructed from elements of self.lower_qpos_limits / self.upper_qpos_limits, which are torch tensors (often on CUDA). Passing 0-d torch tensors into wp_vec6f(...) is likely to fail (or trigger implicit device sync). Convert to Python floats (e.g., .item()) and also add an explicit error if the limits were never initialized (currently this will crash with a non-obvious NoneType/index error).

Suggested change
lower_limits_ = wp_vec6f(
self.lower_qpos_limits[0],
self.lower_qpos_limits[1],
self.lower_qpos_limits[2],
self.lower_qpos_limits[3],
self.lower_qpos_limits[4],
self.lower_qpos_limits[5],
)
upper_limits_ = wp_vec6f(
self.upper_qpos_limits[0],
self.upper_qpos_limits[1],
self.upper_qpos_limits[2],
self.upper_qpos_limits[3],
self.upper_qpos_limits[4],
self.upper_qpos_limits[5],
if self.lower_qpos_limits is None or self.upper_qpos_limits is None:
raise ValueError(
"Joint position limits must be initialized before calling the OPW IK solver."
)
lower_limits_ = wp_vec6f(
float(self.lower_qpos_limits[0].item() if isinstance(self.lower_qpos_limits[0], torch.Tensor) else self.lower_qpos_limits[0]),
float(self.lower_qpos_limits[1].item() if isinstance(self.lower_qpos_limits[1], torch.Tensor) else self.lower_qpos_limits[1]),
float(self.lower_qpos_limits[2].item() if isinstance(self.lower_qpos_limits[2], torch.Tensor) else self.lower_qpos_limits[2]),
float(self.lower_qpos_limits[3].item() if isinstance(self.lower_qpos_limits[3], torch.Tensor) else self.lower_qpos_limits[3]),
float(self.lower_qpos_limits[4].item() if isinstance(self.lower_qpos_limits[4], torch.Tensor) else self.lower_qpos_limits[4]),
float(self.lower_qpos_limits[5].item() if isinstance(self.lower_qpos_limits[5], torch.Tensor) else self.lower_qpos_limits[5]),
)
upper_limits_ = wp_vec6f(
float(self.upper_qpos_limits[0].item() if isinstance(self.upper_qpos_limits[0], torch.Tensor) else self.upper_qpos_limits[0]),
float(self.upper_qpos_limits[1].item() if isinstance(self.upper_qpos_limits[1], torch.Tensor) else self.upper_qpos_limits[1]),
float(self.upper_qpos_limits[2].item() if isinstance(self.upper_qpos_limits[2], torch.Tensor) else self.upper_qpos_limits[2]),
float(self.upper_qpos_limits[3].item() if isinstance(self.upper_qpos_limits[3], torch.Tensor) else self.upper_qpos_limits[3]),
float(self.upper_qpos_limits[4].item() if isinstance(self.upper_qpos_limits[4], torch.Tensor) else self.upper_qpos_limits[4]),
float(self.upper_qpos_limits[5].item() if isinstance(self.upper_qpos_limits[5], torch.Tensor) else self.upper_qpos_limits[5]),

Copilot uses AI. Check for mistakes.
)
wp.launch(
kernel=opw_ik_kernel,
dim=(n_sample),
inputs=(
target_xpos_wp,
self._tcp_inv_warp,
self.params,
self.offsets,
self.sign_corrections,
offsets_,
sign_corrections_,
lower_limits_,
upper_limits_,
self.cfg.safe_margin,
),
outputs=[all_qpos_wp, all_ik_valid_wp],
device=standardize_device_string(self.device),
device=standardize_device_string(kernel_device),
)

if return_all_solutions:
all_qpos = wp.to_torch(all_qpos_wp).reshape(n_sample, N_SOL, DOF)
all_ik_valid = wp.to_torch(all_ik_valid_wp).reshape(n_sample, N_SOL)
return all_ik_valid, all_qpos

if qpos_seed is not None:
qpos_seed_wp = wp.from_torch(qpos_seed.reshape(-1))
if qpos_seed.shape == (
n_sample,
DOF,
):
qpos_seed_ = qpos_seed.to(kernel_device)
elif qpos_seed.shape == (DOF,):
qpos_seed_ = (
qpos_seed.unsqueeze(0).repeat(n_sample, 1).to(kernel_device)
)
else:
logger.log_error(
f"Invalid shape for qpos_seed: {qpos_seed.shape}. Expected ({n_sample}, {DOF}) or ({DOF},)."
)
Comment on lines +323 to +325
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If qpos_seed has an unexpected shape, this logs an error but still proceeds to build qpos_seed_wp from qpos_seed_, which will be undefined and raise at runtime. After logging, return/raise (or fall back to a zeros seed) to avoid an UnboundLocalError.

Suggested change
logger.log_error(
f"Invalid shape for qpos_seed: {qpos_seed.shape}. Expected ({n_sample}, {DOF}) or ({DOF},)."
)
error_msg = (
f"Invalid shape for qpos_seed: {qpos_seed.shape}. "
f"Expected ({n_sample}, {DOF}) or ({DOF},)."
)
logger.log_error(error_msg)
raise ValueError(error_msg)

Copilot uses AI. Check for mistakes.
qpos_seed_wp = wp.from_torch(qpos_seed_)
else:
qpos_seed_wp = wp.zeros(
n_sample * DOF,
dtype=float,
device=standardize_device_string(self.device),
qpos_seed = torch.zeros(
(n_sample, DOF), dtype=torch.float32, device=kernel_device
)
qpos_seed_wp = wp.from_torch(qpos_seed)
all_qpos_wp = all_qpos_wp.reshape((n_sample, N_SOL, DOF))
all_ik_valid_wp = all_ik_valid_wp.reshape((n_sample, N_SOL))
joint_weight = kwargs.get("joint_weight", torch.ones(size=(DOF,), dtype=float))
joint_weight_wp = wp_vec6f(
joint_weight[0],
Expand All @@ -301,25 +341,31 @@ def get_ik_warp(
joint_weight[5],
Comment on lines 334 to 341
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default joint_weight uses torch.ones(..., dtype=float), but dtype must be a torch.dtype (e.g., torch.float32). As written, this will raise a TypeError when joint_weight is not provided. Also consider extracting values via .item() (and/or ensuring the tensor is on CPU) before constructing wp_vec6f to avoid issues with 0-dim CUDA tensors.

Copilot uses AI. Check for mistakes.
)
best_ik_result_wp = wp.zeros(
n_sample * 6, dtype=float, device=standardize_device_string(self.device)
(n_sample, 6), dtype=float, device=standardize_device_string(kernel_device)
)
best_ik_valid_wp = wp.zeros(
n_sample, dtype=int, device=standardize_device_string(self.device)
n_sample, dtype=int, device=standardize_device_string(kernel_device)
)
wp.launch(
kernel=opw_best_ik_kernel,
kernel=opw_ik_select_kernel,
dim=(n_sample),
inputs=[
all_qpos_wp,
all_ik_valid_wp,
qpos_seed_wp,
joint_weight_wp,
],
outputs=[best_ik_result_wp, best_ik_valid_wp],
device=standardize_device_string(self.device),
outputs=[
best_ik_result_wp,
best_ik_valid_wp,
],
device=standardize_device_string(kernel_device),
)

best_ik_result = (
wp.to_torch(best_ik_result_wp).reshape(n_sample, 1, 6).to(self.device)
)
best_ik_result = wp.to_torch(best_ik_result_wp).reshape(n_sample, 1, 6)
best_ik_valid = wp.to_torch(best_ik_valid_wp)
best_ik_valid = wp.to_torch(best_ik_valid_wp).to(self.device)
return best_ik_valid, best_ik_result

def _calculate_dynamic_weights(
Expand Down
Loading
Loading