Skip to content

upgrade pytorch kinematics#244

Open
matafela wants to merge 5 commits intomainfrom
cj/upgrade-pytorch-kinematics
Open

upgrade pytorch kinematics#244
matafela wants to merge 5 commits intomainfrom
cj/upgrade-pytorch-kinematics

Conversation

@matafela
Copy link
Copy Markdown
Collaborator

@matafela matafela commented Apr 23, 2026

Description

  • Upgrade pytorch kinematics solver to 0.10.0
  • Enable torch compiled mode for ik compute.
  • Faster preprocess and postprocess in pytorch ik solver.
  • Update pytorch solver unittest

TODO:

  • Speed up pytorch kinematics ik pre compute and post compute.
  • pytorch solver ik unittest.

Type of change

  • Enhancement (non-breaking change which improves an existing functionality)

Checklist

  • I have run the black . command to format the code base.
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • Dependencies have been updated, if applicable.

Copilot AI review requested due to automatic review settings April 23, 2026 07:45
@matafela matafela marked this pull request as draft April 23, 2026 07:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Upgrades the pytorch_kinematics dependency and enables torch-compiled execution for inverse-kinematics (and now FK) to improve solver performance in the simulation stack.

Changes:

  • Bump pytorch_kinematics from 0.7.6 to 0.10.0.
  • Enable compiled mode for PseudoInverseIK in PytorchSolver.
  • Introduce a torch.compile’d FK path in BaseSolver.get_fk().

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
pyproject.toml Updates pytorch_kinematics version pin to 0.10.0.
embodichain/lab/sim/solvers/pytorch_solver.py Enables compiled execution for the IK solver via use_compile=True.
embodichain/lab/sim/solvers/base_solver.py Adds compiled FK initialization and rewires get_fk() to use it.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 414 to +442
def get_fk(self, qpos: torch.tensor, **kwargs) -> torch.Tensor:
r"""
Computes the forward kinematics for the end-effector link.

Args:
qpos (torch.Tensor): Joint positions. Can be a single configuration (dof,) or a batch (batch_size, dof).
**kwargs: Additional keyword arguments for customization.

Returns:
torch.Tensor: The homogeneous transformation matrix of the end link with TCP applied.
Shape is (4, 4) for single input, or (batch_size, 4, 4) for batch input.
"""
tcp_xpos = torch.as_tensor(
self.tcp_xpos, device=self.device, dtype=torch.float32
)
qpos = torch.as_tensor(qpos, dtype=torch.float32, device=self.device)

if self.pk_serial_chain is None:
logger.log_error("Kinematic chain is not initialized.")
return torch.eye(4, device=self.device)
# Compute forward kinematics
result = self.pk_serial_chain.forward_kinematics(
qpos, end_only=(self.end_link_name is None)
)

# Extract transformation matrices
if isinstance(result, dict):
matrices = result[self.end_link_name].get_matrix()
elif isinstance(result, list):
matrices = torch.stack([xpos.get_matrix().squeeze() for xpos in result])
else:
matrices = result.get_matrix()

# Ensure batch format
if matrices.dim() == 2:
matrices = matrices.unsqueeze(0)

# Create result tensor with proper homogeneous coordinates
result = (
torch.eye(4, device=self.device).expand(matrices.shape[0], 4, 4).clone()
)
result[:, :3, :] = matrices[:, :3, :]
ee_link_xpos = self.compiled_fk(qpos)[-1, :, :, :]

# Ensure batch format for TCP
batch_size = result.shape[0]
batch_size = qpos.shape[0]
tcp_xpos_batch = tcp_xpos.unsqueeze(0).expand(batch_size, -1, -1)

# Apply TCP transformation
return torch.bmm(result, tcp_xpos_batch)
return torch.bmm(ee_link_xpos, tcp_xpos_batch)
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

get_fk now assumes qpos is batched and sets batch_size = qpos.shape[0]. If a caller passes a 1D (dof,) configuration (which the docstring explicitly allows, and which happens in DifferentialSolver.get_ik when qpos_seed is None), batch_size becomes dof and the TCP expansion / bmm shapes will be wrong. Consider normalizing qpos to (1, dof) when qpos.dim()==1, and (optionally) returning a (4,4) matrix for single-input to preserve the documented API.

Copilot uses AI. Check for mistakes.
Comment on lines +174 to +178
self.compiled_fk = torch.compile(
self.pk_serial_chain.forward_kinematics_tensor,
fullgraph=True,
dynamic=True,
)
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

self.compiled_fk is only initialized inside the if self.pk_serial_chain is None: branch. If a pk_serial_chain is provided via kwargs, get_fk() will still call self.compiled_fk(...) and raise AttributeError. Consider always defining self.compiled_fk (e.g., compile when possible, otherwise fall back to pk_serial_chain.forward_kinematics_tensor/forward_kinematics).

Suggested change
self.compiled_fk = torch.compile(
self.pk_serial_chain.forward_kinematics_tensor,
fullgraph=True,
dynamic=True,
)
fk_callable = getattr(self.pk_serial_chain, "forward_kinematics_tensor", None)
if fk_callable is not None:
self.compiled_fk = torch.compile(
fk_callable,
fullgraph=True,
dynamic=True,
)
else:
self.compiled_fk = getattr(self.pk_serial_chain, "forward_kinematics")

Copilot uses AI. Check for mistakes.
Comment on lines 173 to +178
)
self.compiled_fk = torch.compile(
self.pk_serial_chain.forward_kinematics_tensor,
fullgraph=True,
dynamic=True,
)
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

torch.compile(...) is invoked unconditionally during solver construction. The repo doesn't pin a PyTorch version in pyproject.toml, and torch.compile requires PyTorch 2.x; on older installations this will fail at import/runtime. Consider guarding with hasattr(torch, "compile") (or a config flag) and falling back to eager FK when compilation isn't available.

Suggested change
)
self.compiled_fk = torch.compile(
self.pk_serial_chain.forward_kinematics_tensor,
fullgraph=True,
dynamic=True,
)
)
if hasattr(torch, "compile"):
self.compiled_fk = torch.compile(
self.pk_serial_chain.forward_kinematics_tensor,
fullgraph=True,
dynamic=True,
)
else:
self.compiled_fk = self.pk_serial_chain.forward_kinematics_tensor

Copilot uses AI. Check for mistakes.
Comment on lines 170 to +173
max_iterations=self._max_iterations,
lr=self._dt,
num_retries=1,
use_compile=True,
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

The PR description/checklist claims tests were added, but this PR only changes pyproject.toml and solver implementation files (no test diffs). Either include the new/updated tests that validate the pytorch_kinematics upgrade + compile paths, or update the checklist/description to reflect the current PR contents.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 24, 2026 04:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +93 to +95
target_xpos_repeated = target_xpos.unsqueeze(0).repeat(num_samples, 1, 1, 1)
target_xpos_repeated = target_xpos_repeated.reshape(
num_batch * num_samples, 4, 4
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

repeat_target_xpos() repeats along the sample dimension first (unsqueeze(0).repeat(num_samples, ...)) and then reshapes. This produces an interleaved order (sample-major) that does not match the batch-major flattening used by sample() / reshape(-1, dof), so target poses and joint seeds get mismatched when batch_size>1 and num_samples>1. Consider repeating as (batch, num_samples, ...) (e.g., target_xpos[:, None, ...].repeat(1, num_samples, 1, 1).reshape(batch*num_samples, ...)) so each pose stays aligned with its seeds.

Suggested change
target_xpos_repeated = target_xpos.unsqueeze(0).repeat(num_samples, 1, 1, 1)
target_xpos_repeated = target_xpos_repeated.reshape(
num_batch * num_samples, 4, 4
pose_shape = target_xpos.shape[1:]
target_xpos_repeated = target_xpos[:, None, ...].repeat(1, num_samples, 1, 1)
target_xpos_repeated = target_xpos_repeated.reshape(
num_batch * num_samples, *pose_shape

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +75
# seed_random = torch.rand(
# size=(batch_size, n_random_samples, self.dof), device=self.device
# )

# save sampling time, repeat for each batch and sample in one go
seed_random = torch.rand(
size=(1, n_random_samples, self.dof), device=self.device
)
seed_random = seed_random.repeat(batch_size, 1, 1)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

sample() draws seed_random with shape (1, n_random_samples, dof) and then repeat(batch_size, 1, 1), which makes every batch element share identical “random” seeds. This is a behavioral change from the previous per-batch sampling and can reduce IK success rates for batched targets. If you need per-batch randomness, sample with shape (batch_size, n_random_samples, dof) (or use different RNG streams) instead of repeating a single sample.

Suggested change
# seed_random = torch.rand(
# size=(batch_size, n_random_samples, self.dof), device=self.device
# )
# save sampling time, repeat for each batch and sample in one go
seed_random = torch.rand(
size=(1, n_random_samples, self.dof), device=self.device
)
seed_random = seed_random.repeat(batch_size, 1, 1)
seed_random = torch.rand(
size=(batch_size, n_random_samples, self.dof), device=self.device
)

Copilot uses AI. Check for mistakes.
self.pik.initial_config = joint_seed

result = self.pik.solve(tf)
return result.converged_any, result.solutions[:, 0, :].squeeze(0)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

result.solutions[:, 0, :].squeeze(0) will drop the batch dimension when target_pose.shape[0] == 1 (e.g., when num_samples==1), returning a 1D (dof,) tensor. Call sites treat this as (N, dof) and reshape/index accordingly, which will break for the single-sample case. Consider returning result.solutions[:, 0, :] without squeeze(0) to preserve (batch, dof) consistently.

Suggested change
return result.converged_any, result.solutions[:, 0, :].squeeze(0)
return result.converged_any, result.solutions[:, 0, :]

Copilot uses AI. Check for mistakes.
qpos_seed_dis[~all_is_success] = float("inf")
closest_indices = torch.argmin(qpos_seed_dis, dim=1)
closest_qpos = all_results[torch.arange(batch_size), closest_indices]
return all_is_success.any(dim=0), closest_qpos[:, None, :]
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

For the non-return_all_solutions path, return all_is_success.any(dim=0), ... reduces over the batch dimension and returns a (num_samples,) tensor rather than (batch_size,). This breaks the documented/expected API shape (and differs from the return_all_solutions path which uses dim=1). This should likely be all_is_success.any(dim=1) so success is reported per target in the batch.

Suggested change
return all_is_success.any(dim=0), closest_qpos[:, None, :]
return all_is_success.any(dim=1), closest_qpos[:, None, :]

Copilot uses AI. Check for mistakes.
Comment on lines +174 to 179
self.compiled_fk = torch.compile(
self.pk_serial_chain.forward_kinematics_tensor,
fullgraph=True,
dynamic=True,
)

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

self.compiled_fk is only initialized when pk_serial_chain is created inside BaseSolver.__init__. If a caller injects an existing pk_serial_chain via kwargs, get_fk() will later raise AttributeError because self.compiled_fk was never set. Consider compiling (or assigning a non-compiled fallback) whenever self.pk_serial_chain is provided, or guard get_fk() to call the non-compiled FK path when compiled_fk is missing.

Suggested change
self.compiled_fk = torch.compile(
self.pk_serial_chain.forward_kinematics_tensor,
fullgraph=True,
dynamic=True,
)
self.compiled_fk = torch.compile(
self.pk_serial_chain.forward_kinematics_tensor,
fullgraph=True,
dynamic=True,
)

Copilot uses AI. Check for mistakes.
start = time.perf_counter()
ik_success, ik_qpos = solver.get_ik(
fk_xpos,
joint_seed=qpos_seed,
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

PytorchSolver.get_ik() takes qpos_seed as its seed argument, but the benchmark calls it with joint_seed=..., which will be swallowed by **kwargs and ignored. This means the benchmark is not using the intended seed values. Update the call to pass qpos_seed=qpos_seed (or rename the solver parameter to joint_seed to match the base-class API).

Suggested change
joint_seed=qpos_seed,
qpos_seed=qpos_seed,

Copilot uses AI. Check for mistakes.
@matafela matafela marked this pull request as ready for review April 24, 2026 07:14
Copilot AI review requested due to automatic review settings April 24, 2026 07:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyproject.toml
"casadi",
"qpsolvers[osqp]==4.8.1",
"pytorch_kinematics==0.7.6",
"pytorch_kinematics==0.10.0",
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The codebase now uses torch.compile (e.g., in BaseSolver), but torch itself isn’t declared/pinned in dependencies. If PyTorch 2.x is required for this PR’s compiled-mode features, consider declaring a minimum supported torch version (or documenting/enforcing it elsewhere) to avoid runtime failures on older installations.

Suggested change
"pytorch_kinematics==0.10.0",
"pytorch_kinematics==0.10.0",
"torch>=2.0",

Copilot uses AI. Check for mistakes.
Comment on lines +435 to 439
ee_link_xpos = self.compiled_fk(qpos)[-1, :, :, :]

# Ensure batch format for TCP
batch_size = result.shape[0]
batch_size = qpos.shape[0]
tcp_xpos_batch = tcp_xpos.unsqueeze(0).expand(batch_size, -1, -1)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

get_fk assumes qpos is 2D (batch, dof) (uses qpos.shape[0] as batch and indexes compiled_fk(qpos)[-1, :, :, :]). This breaks the documented single-config input (dof,). Consider normalizing qpos to 2D (unsqueeze when 1D) and optionally squeezing the output back for the single-input case.

Copilot uses AI. Check for mistakes.
Comment on lines +301 to +303
k = torch.ceil((self.lower_qpos_limits - qpos) / two_pi)
qpos_mapped = qpos + k * two_pi
is_within_limits = (qpos_mapped >= self.lower_qpos_limits) & (
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

_qpos_map_to_limits uses k = ceil((lower - q)/2π), which only guarantees qpos_mapped >= lower. For joints with a valid range smaller than 2π, this can incorrectly mark a wrap-able value as invalid even though a different multiple of 2π would land within [lower, upper]. Consider deriving an integer k range using both bounds and selecting a valid k when one exists.

Suggested change
k = torch.ceil((self.lower_qpos_limits - qpos) / two_pi)
qpos_mapped = qpos + k * two_pi
is_within_limits = (qpos_mapped >= self.lower_qpos_limits) & (
k_min = torch.ceil((self.lower_qpos_limits - qpos) / two_pi)
k_max = torch.floor((self.upper_qpos_limits - qpos) / two_pi)
has_valid_wrap = k_min <= k_max
# Select a valid wrap when one exists. Using k_min preserves the previous
# behavior of choosing the smallest shift that satisfies the lower bound,
# while also ensuring the selected shift satisfies the upper bound.
k = torch.where(has_valid_wrap, k_min, k_min)
qpos_mapped = qpos + k * two_pi
is_within_limits = has_valid_wrap & (qpos_mapped >= self.lower_qpos_limits) & (

Copilot uses AI. Check for mistakes.

Args:
qpos_limits: tensor of shape (1, n, 2) or (n, 2) where each row is [low, high].
steps_per_joint: number of values per joint (defaults to 2: low and high).
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

Docstring mismatch: steps_per_joint defaults to 4 in the function signature, but the docstring says it defaults to 2. Please update the docstring (or the default) so documentation matches behavior.

Suggested change
steps_per_joint: number of values per joint (defaults to 2: low and high).
steps_per_joint: number of values per joint (defaults to 4).

Copilot uses AI. Check for mistakes.
@matafela matafela requested a review from yuecideng April 24, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants