Skip to content

Update base planner#173

Open
matafela wants to merge 5 commits intomainfrom
cj/update-base-planner
Open

Update base planner#173
matafela wants to merge 5 commits intomainfrom
cj/update-base-planner

Conversation

@matafela
Copy link
Collaborator

Description

Update BasePlanner.

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 March 11, 2026 08:48
@matafela matafela requested a review from yuecideng March 11, 2026 08:48
Copy link
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

This PR updates the planning API to use a structured PlanState object (instead of raw dicts) and adapts the TOPPRA planner and motion generator to the new interface.

Changes:

  • Introduces PlanState (plus MoveType / MovePart) in planner utils.
  • Refactors BasePlanner and ToppraPlanner to accept PlanState inputs and initializes planners via **kwargs.
  • Updates MotionGenerator to construct PlanState objects before invoking the planner.

Reviewed changes

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

File Description
embodichain/lab/sim/planners/utils.py Adds PlanState and related enums used to represent planning inputs more explicitly.
embodichain/lab/sim/planners/base_planner.py Updates base planner API to accept PlanState and switches init to **kwargs.
embodichain/lab/sim/planners/toppra_planner.py Updates TOPPRA planner to accept PlanState and adjusts waypoint handling accordingly.
embodichain/lab/sim/planners/motion_generator.py Packs dict-based states into PlanState before calling the underlying planner.

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

Comment on lines +80 to +84
move_part: MovePart = MovePart.LEFT
xpos: torch.Tensor = None
qpos: torch.Tensor = None
qacc: torch.Tensor = None
qvel: torch.Tensor = None
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

PlanState fields are annotated as torch.Tensor but default to None (and are also populated with lists in MotionGenerator.plan). Consider widening these types (e.g., torch.Tensor | np.ndarray | Sequence[float] | None) and/or using Optional[...] to match the actual inputs and avoid type-checking/runtime surprises.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to 41
def __init__(self, **kwargs):
self.dofs = kwargs.get("dofs", None)
self.max_constraints = kwargs.get("max_constraints", None)

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

BasePlanner.__init__ now silently accepts missing dofs / max_constraints (setting them to None), which will later crash in places like constraint checks or waypoint validation. Since these are required for any planner, consider making them explicit parameters again or validating kwargs and raising via logger.log_error when required values are absent.

Suggested change
def __init__(self, **kwargs):
self.dofs = kwargs.get("dofs", None)
self.max_constraints = kwargs.get("max_constraints", None)
def __init__(self, **kwargs):
# Required configuration parameters for any planner
self.dofs = kwargs.get("dofs", None)
self.max_constraints = kwargs.get("max_constraints", None)
missing_args = []
if self.dofs is None:
missing_args.append("dofs")
if self.max_constraints is None:
missing_args.append("max_constraints")
if missing_args:
logger.log_error(
f"Missing required planner initialization argument(s): {', '.join(missing_args)}",
ValueError,
)

Copilot uses AI. Check for mistakes.
Comment on lines 43 to 47
def plan(
self,
current_state: Dict,
target_states: List[Dict],
current_state: PlanState,
target_states: list[PlanState],
**kwargs,
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The plan signature has been updated to use PlanState, but the docstring (Args section) still describes current_state / target_states as dictionaries. Please update the docstring to match the new PlanState-based API so callers know what fields are expected.

Copilot uses AI. Check for mistakes.
Comment on lines 46 to 51
# Create TOPPRA-specific constraint arrays (symmetric format)
# This format is required by TOPPRA library
max_constraints = kwargs.get("max_constraints", None)
self.vlims = np.array([[-v, v] for v in max_constraints["velocity"]])
self.alims = np.array([[-a, a] for a in max_constraints["acceleration"]])

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

max_constraints is fetched from kwargs and then indexed without validation; if it’s missing/None this will raise a non-obvious TypeError. Since BasePlanner already stores self.max_constraints, consider using that and explicitly validating it (and self.dofs) with logger.log_error when they are not provided.

Suggested change
# Create TOPPRA-specific constraint arrays (symmetric format)
# This format is required by TOPPRA library
max_constraints = kwargs.get("max_constraints", None)
self.vlims = np.array([[-v, v] for v in max_constraints["velocity"]])
self.alims = np.array([[-a, a] for a in max_constraints["acceleration"]])
# Validate required planner configuration
if self.max_constraints is None:
logger.log_error(
"ToppraPlanner requires 'max_constraints' to be provided in BasePlanner.",
ValueError,
)
if self.dofs is None:
logger.log_error(
"ToppraPlanner requires 'dofs' to be provided in BasePlanner.",
ValueError,
)
# Create TOPPRA-specific constraint arrays (symmetric format)
# This format is required by TOPPRA library
try:
velocity_limits = self.max_constraints["velocity"]
acceleration_limits = self.max_constraints["acceleration"]
except (TypeError, KeyError):
logger.log_error(
"ToppraPlanner 'max_constraints' must be a mapping with "
"'velocity' and 'acceleration' entries.",
ValueError,
)
self.vlims = np.array([[-v, v] for v in velocity_limits])
self.alims = np.array([[-a, a] for a in acceleration_limits])

Copilot uses AI. Check for mistakes.
Comment on lines 99 to 103
np.array([[0.0] * self.dofs, [0.0] * self.dofs]),
np.array([[0.0] * self.dofs, [0.0] * self.dofs]),
0,
0,
)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

In the early-return case for identical waypoints, times and duration are returned as 0 (ints). Callers (and the BasePlanner.plan return type) expect times to be an array-like and duration to be a float; please return consistent types (e.g., a 2-element times array and a float duration) to avoid downstream type/shape errors.

Copilot uses AI. Check for mistakes.
Comment on lines +262 to +263
qvel=current_state["velocity"],
qacc=current_state["acceleration"],
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

MotionGenerator.plan previously worked when callers provided only position (docstring says velocity/acceleration are optional), but this new packing code unconditionally indexes current_state["velocity"] and ["acceleration"], which will raise KeyError for minimal inputs. Please default missing values (e.g., zeros) or reuse _create_state_dict-style normalization before building PlanState.

Suggested change
qvel=current_state["velocity"],
qacc=current_state["acceleration"],
qvel=current_state.get("velocity", np.zeros(self.dof)),
qacc=current_state.get("acceleration", np.zeros(self.dof)),

Copilot uses AI. Check for mistakes.
Comment on lines 52 to 56
def plan(
self,
current_state: dict,
target_states: list[dict],
current_state: PlanState,
target_states: list[PlanState],
**kwargs,
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The plan method now takes PlanState objects, but the docstring still describes current_state / target_states as dictionaries with keys like 'position'. Please update the docstring to reflect the new PlanState fields (e.g., qpos) so the public API stays accurate.

Copilot uses AI. Check for mistakes.
from typing import Union
from embodichain.utils import logger
import torch
from enum import Enum
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

There is a duplicated from enum import Enum import here (lines 17 and 21). Please remove the duplicate and keep imports grouped/ordered to avoid confusion and unnecessary re-imports.

Suggested change
from enum import Enum

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 12, 2026 04:06
Copy link
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 4 out of 4 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

embodichain/lab/sim/planners/toppra_planner.py:63

  • plan() now takes PlanState, but the docstring still refers to state dictionaries. Please update the parameter documentation to match PlanState (e.g., which fields are required such as qpos).
        r"""Execute trajectory planning.

        Args:
            current_state: Dictionary containing 'position', 'velocity', 'acceleration' for current state
            target_states: List of dictionaries containing target states


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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 27 to +40
@@ -34,15 +35,15 @@ class BasePlanner(ABC):
max_constraints: Dictionary containing 'velocity' and 'acceleration' constraints
"""

def __init__(self, dofs: int, max_constraints: Dict[str, List[float]]):
self.dofs = dofs
self.max_constraints = max_constraints
def __init__(self, **kwargs):
self.dofs = kwargs.get("dofs", None)
self.max_constraints = kwargs.get("max_constraints", None)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The class docstring for BasePlanner (and its Args: section) still documents dofs/max_constraints as explicit constructor parameters, but __init__ now takes **kwargs. Please update the documentation (or restore explicit parameters) to avoid confusing subclasses/callers.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to 103
if len(current_state.qpos) != self.dofs:
logger.log_info("Current wayponit does not align")
return False, None, None, None, None, None
for target in target_states:
if len(target["position"]) != self.dofs:
if len(target.qpos) != self.dofs:
logger.log_info("Target Wayponits does not align")
return False, None, None, None, None, None

if (
len(target_states) == 1
and np.sum(
np.abs(
np.array(target_states[0]["position"])
- np.array(current_state["position"])
)
np.abs(np.array(target_states[0].qpos) - np.array(current_state.qpos))
)
< 1e-3
):
logger.log_info("Only two same waypoints, do not plan")
return (
True,
np.array([current_state["position"], target_states[0]["position"]]),
np.array([current_state.qpos, target_states[0].qpos]),
np.array([[0.0] * self.dofs, [0.0] * self.dofs]),
np.array([[0.0] * self.dofs, [0.0] * self.dofs]),
0,
0,
)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

Several failure/degenerate returns from ToppraPlanner.plan() don’t match the declared return contract: (1) waypoint-dimension mismatch returns duration=None; (2) the "same waypoints" fast-path returns times=0 and duration=0 instead of an array of timestamps and a float duration. Please standardize these returns (e.g., times=np.array([...]) and duration=0.0) so callers can rely on types even when planning is skipped/failed.

Copilot uses AI. Check for mistakes.
Comment on lines 17 to 23
from enum import Enum
from typing import Union
from embodichain.utils import logger
import torch
from enum import Enum
from dataclasses import dataclass

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

Enum is imported twice and the import block is out of order (stdlib/third-party/local). Please remove the duplicate from enum import Enum and re-order imports to avoid lint/format churn.

Suggested change
from enum import Enum
from typing import Union
from embodichain.utils import logger
import torch
from enum import Enum
from dataclasses import dataclass
from dataclasses import dataclass
from enum import Enum
from typing import Union
import torch
from embodichain.utils import logger

Copilot uses AI. Check for mistakes.
Comment on lines 17 to 28
import torch
import numpy as np
from typing import Dict, List, Tuple, Union, Any
from enum import Enum
from scipy.spatial.transform import Rotation, Slerp

from embodichain.lab.sim.planners.toppra_planner import ToppraPlanner
from embodichain.lab.sim.planners.utils import TrajectorySampleMethod
from embodichain.lab.sim.objects.robot import Robot
from embodichain.utils import logger


class PlannerType(Enum):
r"""Enumeration for different planner types."""

TOPPRA = "toppra"
"""TOPPRA planner for time-optimal trajectory planning."""
from embodichain.lab.sim.planners.utils import PlanState, MoveType, MovePart

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

After removing PlannerType, from enum import Enum appears unused in this module. Please drop the unused import to avoid lint failures and keep the import list accurate.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +44
def __init__(self, **kwargs):
r"""Initialize the TOPPRA trajectory planner.

Args:
dofs: Number of degrees of freedom
max_constraints: Dictionary containing 'velocity' and 'acceleration' constraints
"""
super().__init__(dofs, max_constraints)
super().__init__(**kwargs)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

ToppraPlanner.__init__ now accepts only **kwargs, but the docstring still documents positional dofs/max_constraints arguments. Please update the docstring (and ideally add explicit keyword-only parameters) so it’s clear what must be provided.

Copilot uses AI. Check for mistakes.
chenjian added 2 commits March 12, 2026 14:38
Copilot AI review requested due to automatic review settings March 12, 2026 07:13
Copy link
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 4 out of 4 changed files in this pull request and generated 6 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 89 to 104
if (
len(target_states) == 1
and np.sum(
np.abs(
np.array(target_states[0]["position"])
- np.array(current_state["position"])
)
np.abs(np.array(target_states[0].qpos) - np.array(current_state.qpos))
)
< 1e-3
):
logger.log_info("Only two same waypoints, do not plan")
return (
True,
np.array([current_state["position"], target_states[0]["position"]]),
np.array([current_state.qpos, target_states[0].qpos]),
np.array([[0.0] * self.dofs, [0.0] * self.dofs]),
np.array([[0.0] * self.dofs, [0.0] * self.dofs]),
0,
0,
)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

In the "two same waypoints" early-return branch, the function returns NumPy arrays and scalar 0s for times/duration. After this PR, callers (e.g., MotionGenerator.create_discrete_trajectory) assume positions/times are torch tensors and will crash when calling .to(...). Please make this early-return match the new contract (torch tensors on self.device, and a float duration).

Copilot uses AI. Check for mistakes.
Comment on lines 24 to 25
from typing import TYPE_CHECKING, Union, Tuple

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

TYPE_CHECKING, Union, and Tuple are imported but unused in this module. Please remove them to avoid dead imports and keep lint/static analysis clean.

Suggested change
from typing import TYPE_CHECKING, Union, Tuple

Copilot uses AI. Check for mistakes.
Comment on lines 106 to 110
# Build waypoints
waypoints = [np.array(current_state["position"])]
waypoints = [np.array(current_state.qpos)]
for target in target_states:
waypoints.append(np.array(target["position"]))
waypoints.append(np.array(target.qpos))
waypoints = np.array(waypoints)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

np.array(current_state.qpos) / np.array(target.qpos) will produce a dtype=object array when qpos is a torch.Tensor (common in this codebase), which can break TOPPRA interpolation. Please explicitly convert torch tensors to NumPy (e.g., .detach().cpu().numpy()) before building waypoints.

Copilot uses AI. Check for mistakes.
Comment on lines 59 to 81
@@ -65,45 +70,16 @@
self.robot = robot
self.sim = sim
self.collision_margin = collision_margin
self.uid = uid
self.uid = uid # control part

# Get robot DOF using get_joint_ids for specified control part (None for whole body)
self.dof = len(robot.get_joint_ids(uid))

# Create planner based on planner_type
self.planner_type = self._parse_planner_type(planner_type)
self.planner = self._create_planner(
self.planner_type, default_velocity, default_acceleration, **kwargs
planner_type, default_velocity, default_acceleration, **kwargs
)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

MotionGenerator used to accept planner types case-insensitively (and via PlannerType enum). After removing _parse_planner_type, planner_type must exactly match a key in _support_planner_dict, so values like "TOPPRA" will now fail. Please normalize (e.g., planner_type = planner_type.lower()) and keep the previous validation/warning behavior to avoid an API regression.

Copilot uses AI. Check for mistakes.
Comment on lines 472 to +476
if not success or positions is None:
logger.log_error("Failed to plan trajectory")

# Convert positions to list
out_qpos_list = (
positions.tolist() if isinstance(positions, np.ndarray) else positions
)

out_qpos_list = positions.to("cpu").numpy().tolist()
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

out_qpos_list = positions.to("cpu").numpy().tolist() assumes positions is always a torch.Tensor. With the current ToppraPlanner.plan early-return branch, positions can be a NumPy array, causing an AttributeError here. Either enforce the torch return type in all planners (preferred) or add a safe conversion/branch here.

Copilot uses AI. Check for mistakes.
Comment on lines 45 to 49
def plan(
self,
current_state: Dict,
target_states: List[Dict],
current_state: PlanState,
target_states: list[PlanState],
**kwargs,
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The plan() docstring still describes current_state / target_states as dictionaries, but the signature now uses PlanState and the return types are torch tensors. Please update the docstring to match the new API to avoid misleading callers.

Copilot uses AI. Check for mistakes.
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