Skip to content

Breaking API changes

Changes that break existing Python code, newest first, each with what replaces it. 0.35.0.9 removes IfoptQPProblem and IfoptProblem: build a TrajOptQPProblem instead, and expect different results wherever a cost went in without a penalty type. Its convex evaluators take only the QP solution vector. The changelog lists every change; the upgrade guide covers the tesseract upgrades in full.

Release What breaks Use instead
Unreleased CollisionCoeffData.setPairCollisionCoeff / getPairCollisionCoeff removed The C++ names
Unreleased CollisionCoeffData, TrajOptCollisionConfig, SQPParameters compare by value and are unhashable A stable key, or id()
Unreleased A collision constraint on a Var outside a NodesVariables raises VarNotInVariableSetError (a ValueError) at construction instead of segfaulting on its first update() Hand the Node over first
Unreleased Node.getParent raises NodeParentUnavailableError for planner-owned nodes, including inside create overrides Keep the planner's Nodes, not their set
Unreleased OSQPEigenSolver.update* raise QPSizeError on a wrong-length vector Size by init()
Unreleased 16 command-language waypoint, instruction and Poly types compare by value and are unhashable A stable key, or id()
Unreleased TOTGCompositeProfile, ISPCompositeProfile, ISPMoveProfile, ConstantTCPSpeedCompositeProfile compare by value and are unhashable A stable key, or id()
Unreleased The seven simple-planner move profiles compare by value and are unhashable A stable key, or id()
Unreleased OMPLSolverConfig.getNumPlanners() removed len(cfg.planners)
Unreleased OMPL, TrajOpt and task-composer planning value types compare by value and are unhashable A stable key, or id()
Unreleased create_ompl_planner_configurators raises ValueError on a keyword none of the requested planners has Drop the keyword
Unreleased 7 TrajOpt-Ifopt and Descartes profiles and waypoint configs compare by value and are unhashable A stable key, or id()
Unreleased Overriding a hook (generate, create, create*) on a Python subclass of a concrete profile raises ProfileOverrideNotDispatchedError at class definition; the override was silently never called Subclass the abstract base and hold the concrete profile
0.35.0.9 ChangeCollisionMarginsCommand(pair_margin_data) merges the pair margins (MODIFY) instead of replacing them Pass REPLACE
0.35.0.9 MotionProgram / get_manipulator_info require tcp_frame and working_frame; fk / ik require tip_link; to_composite_instruction has no tcp_frame Name the frames
0.35.0.9 tesseract_geometry.PointCloudPoint renamed PointCloud.Point
0.35.0.9 tesseract_scene_graph.JointType_* constants removed The JointType members
0.35.0.9 37 collision, common and environment value types compare by value and are unhashable: hash(), set members and dict keys raise TypeError A stable key, or id()
0.35.0.9 TaskComposerPluginFactory.createTaskComposerNode / createTaskComposerExecutor no longer return None except TaskComposerPluginError
0.35.0.9 margin_data_override_type, set{Default,Pair}CollisionMarginData, CollisionMarginOverrideType, get/setPairCollisionMargin removed The tesseract names
0.35.0.9 ContactTestType_*, Events_*, ModifyAllowedCollisionsType_* and module-level CONSOLE_BRIDGE_LOG_* constants removed The enum members
0.35.0.9 tesseract_environment.AnyPoly_wrap_EnvironmentConst, tesseract_command_language.AnyPoly_* and _HAS_TASK_COMPOSER removed tesseract_task_composer.AnyPoly_*
0.35.0.9 FilesystemPath, _FilesystemPath, TransformMap, Environment.initFromUrdf / initFromUrdfSrdf removed; Environment.init(str, …) is content pathlib.Path
0.35.0.9 evaluateConvexCosts, evaluateTotalConvexCost, evaluateConvexConstraintViolations raise ValueError on a var_vals that is not getNumQPVars() long The QP solution vector
0.35.0.9 trajopt_sqp.IfoptQPProblem and trajopt_sqp.IfoptProblem removed TrajOptQPProblem
0.35.0.1 planning.Transform removed; Pose is an Isometry3d Pose
0.35.0.1 tesseract 0.35: package://tesseract_support/ resource URIs package://tesseract/support/
0.34.1.0 tesseract 0.34: ifopt module, JointPosition, CartPosInfo, CollisionCache, … 0.33 → 0.34 guide

Unreleased

setPairCollisionCoeff and getPairCollisionCoeff removed

trajopt_ifopt.CollisionCoeffData bound two copies of its setter and getter under names C++ does not have (#298). Call the C++ names:

before after
data.setPairCollisionCoeff(a, b, c) data.setCollisionCoeff(a, b, c)
data.getPairCollisionCoeff(a, b) data.getCollisionCoeff(a, b)

trajopt value types compare by value and are unhashable

trajopt_ifopt.CollisionCoeffData, trajopt_ifopt.TrajOptCollisionConfig and trajopt_sqp.SQPParameters bind __eq__/__ne__ from their C++ operator== and set __hash__ = None (#261), as the 0.35.0.9 collision, common and environment types did. CollisionCoeffData() == CollisionCoeffData() was False. hash(obj), {obj} and d[obj] = … now raise TypeError: unhashable type:

before after
seen = {config} seen = {id(config)}
cache[params] = result key on the fields you vary, e.g. cache[params.max_iterations] = result

Collision constraints need a Var in a variable set

trajopt's collision constraints key their contact cache on the variable set's hash, which they read through var.getParent().getParent() without a check: a Var outside a NodesVariables dereferenced null on the first update(). DiscreteCollisionConstraint, DiscreteCollisionConstraintD, DiscreteCollisionNumericalConstraint, ContinuousCollisionConstraint, ContinuousCollisionConstraintD and ContinuousCollisionNumericalConstraint now raise VarNotInVariableSetError (a ValueError) at construction (#297). A Var from createNodesVariables always qualifies; one from Node.addVar qualifies once its Node is handed to NodesVariables:

before after
var = node.addVar(...); DiscreteCollisionConstraint(evaluator, var); NodesVariables("t", [node]) var = node.addVar(...); NodesVariables("t", [node]); DiscreteCollisionConstraint(evaluator, var)

Node.getParent raises for planner-owned nodes

A Python TrajOptIfoptCompositeProfile.create receives the planner's Nodes, whose variable set the planner builds in C++ and frees when its solve returns. Node.getParent() on such a Node returned a view of that set, which read freed memory once the solve was over (#297). It now raises NodeParentUnavailableError (a RuntimeError) for every Node of a set C++ owns, during create too. getParent() still returns the set for Nodes of a set built from Python:

before after
nodes[0].getParent().getRows() in create read what create needs from the Nodes themselves: sum(node.size() for node in nodes)

OSQPEigenSolver updates check sizes

updateGradient, updateLowerBound, updateUpperBound and updateBounds now sit on QPSolver and raise QPSizeError (a ValueError) unless the vector has init()'s num_vars (gradient) or num_cnts (bounds) entries (#283). trajopt checked with assert only and read past a short vector. Call init(num_vars, num_cnts) with the QP's size before the updates.

Command-language types compare by value and are unhashable

The command-language waypoints, instructions and Polys compared by identity: two JointWaypoint(["a"], [1.0]) were unequal. They now bind __eq__/__ne__ from the C++ operators and set __hash__ = None, as the collision, common and environment types did in 0.35.0.9 (#261): CartesianWaypoint, JointWaypoint, StateWaypoint, WaypointPoly, CartesianWaypointPoly, JointWaypointPoly, StateWaypointPoly, InstructionPoly, MoveInstructionPoly, MoveInstruction, CompositeInstruction, WaitInstruction, TimerInstruction, SetAnalogInstruction, SetDigitalInstruction, SetToolInstruction.

hash(obj), {obj} and d[obj] = … now raise TypeError: unhashable type. Every instruction has a UUID, which is the natural key; MoveInstruction equality also compares it, so two moves built separately are unequal until the UUIDs match:

before after
seen = {move} seen = {move.getUUID()}
cache[waypoint] = result cache[id(waypoint)] = result (keep waypoint alive)
move in done meant identity any(m is move for m in done); in now compares values

Time-parameterization profiles compare by value and are unhashable

The four time-parameterization profiles were bound without their C++ operator==, so == compared identity: TOTGCompositeProfile() == TOTGCompositeProfile() was False. They now bind __eq__/__ne__ and set __hash__ = None (#261), as the collision, common and environment value types did in 0.35.0.9. The new RuckigTrajectorySmoothingCompositeProfile behaves the same way.

before after
{TOTGCompositeProfile(): …}, set_of_profiles.add(p) key by name, e.g. {"DEFAULT": p}, or by id(p)
p == q (identity) p is q

Simple-planner move profiles compare by value and are unhashable

SimplePlannerFixedSize{,Assign,AssignNoIK}MoveProfile and SimplePlannerLVS{,NoIK,Assign,AssignNoIK}MoveProfile bind the C++ operator== (#261): two profiles with the same fields are ==, and hash(), set members and dict keys raise TypeError, as for the value types in 0.35.0.9.

before after
{profile: name} {id(profile): name}, or key by the profile name
profile in seen_set (identity) any(p is profile for p in seen)

OMPLSolverConfig.getNumPlanners() removed

OMPLSolverConfig.planners is bound (#302), so the count is len(cfg.planners). The list is copied on access; its configurators are shared with the config. cfg.planners.append(x) changes the copy, but cfg.planners[0].range = 0.5 edits the config. Assign a new list, or use addPlanner / clearPlanners, which change it in place.

before after
cfg.getNumPlanners() len(cfg.planners)

Planner value types compare by value and are unhashable

As for collision, common and environment in 0.35.0.9, these classes bind the C++ operator== and set __hash__ = None (#261). hash(), set members and dict keys raise TypeError:

module classes
tesseract_motion_planners_ompl OMPLSolverConfig, OMPLRealVectorMoveProfile
tesseract_motion_planners_trajopt TrajOptCartesianWaypointConfig, TrajOptJointWaypointConfig, TrajOptDefaultMoveProfile, TrajOptDefaultCompositeProfile, TrajOptOSQPSolverProfile, BasicTrustRegionSQPParameters (and the new OSQPSettings)
tesseract_task_composer_planning ContactCheckProfile, FixStateBoundsProfile, FixStateCollisionProfile, KinematicLimitsCheckProfile, MinLengthProfile, ProfileSwitchProfile, UpsampleTrajectoryProfile

Key on a field, or on id(obj) where identity is meant, as in the 0.35.0.9 entry. OMPLSolverConfig compares planners by count only (upstream ompl_solver_config.cpp), so two configs holding different configurator types of the same number are equal.

create_ompl_planner_configurators rejects unused keywords

The helper accepts every OMPLPlannerType name (#301) and sets each keyword on the requested configurators that have that field. It used to ignore a keyword no configurator used; now it raises ValueError naming the keyword. A keyword that fits at least one requested planner is still accepted (goal_bias with ["RRTConnect", "RRTstar"] sets it on the RRTstar ones).

before after
create_ompl_planner_configurators(["RRTConnect"], goal_bias=0.1) create_ompl_planner_configurators(["RRTConnect"])

TrajOpt-Ifopt and Descartes profiles compare by value and are unhashable

These classes bind __eq__/__ne__ from their C++ operator== and set __hash__ = None, as the 0.35.0.9 value types do (#261):

| tesseract_motion_planners_trajopt_ifopt | TrajOptIfoptCartesianWaypointConfig, TrajOptIfoptJointWaypointConfig, TrajOptIfoptDefaultMoveProfile, TrajOptIfoptDefaultCompositeProfile, TrajOptIfoptOSQPSolverProfile | | tesseract_motion_planners_descartes | DescartesDefaultMoveProfileD, DescartesLadderGraphSolverProfileD |

hash(profile), {profile} and d[profile] = … raise TypeError: unhashable type. Key on the profile's name in the ProfileDictionary, or on id(profile) where identity is what you mean:

| seen = {profile} | seen = {profile_name} | | cache[profile] = result | cache[id(profile)] = result (keep profile alive) |

Overriding a hook on a concrete profile raises

Only the abstract profile bases forward the planner's calls to Python. A Python subclass of a concrete profile (the seven simple-planner move profiles, TrajOptIfoptDefaultMoveProfile, TrajOptIfoptDefaultCompositeProfile, TrajOptIfoptOSQPSolverProfile, DescartesDefaultMoveProfileD, DescartesLadderGraphSolverProfileD) that defined generate, create or a create* hook planned with the C++ hook and never ran the override. Such a class statement now raises ProfileOverrideNotDispatchedError, a TypeError (#340):

before after
class P(SimplePlannerLVSMoveProfile): def generate(...) class P(SimplePlannerMoveProfile), holding a SimplePlannerLVSMoveProfile and calling its generate
class P(TrajOptIfoptDefaultCompositeProfile): def create(...) class P(TrajOptIfoptCompositeProfile), holding a TrajOptIfoptDefaultCompositeProfile and calling its create
class P(TrajOptIfoptOSQPSolverProfile): def create(...) configure the solver through the profile's fields and callbacks

A subclass that defines no hook (to add fields or helpers) is unaffected.

0.35.0.9

ChangeCollisionMarginsCommand(pair_margin_data) defaults to MODIFY

The binding defaulted override_type to REPLACE, so leaving it out replaced every pair margin in the environment, where the same C++ call merges the given pairs into the existing ones (#247). Both overloads that take pair_margin_data now default to MODIFY, as upstream. Code that left override_type out and relied on the replace passes it explicitly:

before after
ChangeCollisionMarginsCommand(pairs) ChangeCollisionMarginsCommand(pairs, CollisionMarginPairOverrideType.REPLACE)

CollisionMarginPairOverrideType is in tesseract_common. The new ChangeCollisionMarginsCommand(default_margin, pairs, override_type) overload defaults to MODIFY too.

The planning API no longer picks a TCP or working frame

tesseract_robotics.planning used to fill in frames it could not know (#30): the TCP as the last of getActiveLinkNames(), which is only the tip for a serial chain, and the working frame as the literal "base_link". get_manipulator_info also cached the guess and returned one shared, mutable ManipulatorInfo per key. Now the caller names every frame: a missing one raises TypeError at the call.

before after
MotionProgram("manipulator") MotionProgram("manipulator", tcp_frame="tool0", working_frame="base_link")
MotionProgram("manipulator", tcp_frame="tool0") MotionProgram("manipulator", tcp_frame="tool0", working_frame="base_link")
robot.get_manipulator_info("manipulator") robot.get_manipulator_info("manipulator", "tool0", "base_link")
robot.fk("manipulator", q) robot.fk("manipulator", q, "tool0")
robot.ik("manipulator", pose, seed=q) robot.ik("manipulator", pose, "tool0", seed=q)
robot.ik("manipulator", pose, q, "tool0") (positional seed, tip_link) robot.ik("manipulator", pose, "tool0", q)
program.to_composite_instruction(joint_names, "tool0") program.to_composite_instruction(joint_names)

For the bundled abb_irb2400 and lbr_iiwa_14_r820 the old guesses were "tool0" and "base_link", so the right-hand column behaves exactly as before. For another robot, pass the link your tool is mounted on and the frame your Cartesian targets are expressed in.

New: MotionProgram and get_manipulator_info take a keyword-only tcp_offset, a named TCP (SRDF group_tcps, #133) or an Isometry3d, and the planner puts tcp_frame * tcp_offset on each Cartesian target. See Frames and the TCP.

PointCloudPoint renamed to PointCloud.Point

The point type of PointCloud is nested in C++ (tesseract::geometry::PointCloud::Point) and is now bound nested (#209). The module-level PointCloudPoint is gone: importing it raises ImportError, reading it as an attribute raises AttributeError. addPoint(x, y, z) is unchanged.

before after
PointCloudPoint(), PointCloudPoint(x, y, z) PointCloud.Point(), PointCloud.Point(x, y, z)
from tesseract_robotics.tesseract_geometry import PointCloudPoint from tesseract_robotics.tesseract_geometry import PointCloud, then PointCloud.Point

SWIG-era JointType_* constants removed

tesseract_scene_graph no longer exports a module constant per JointType value. The seven constants were copies kept from the SWIG bindings; C++ has none of them, and JointType is bound as an enum whose members are the same objects (#214, finishing #176). Importing one now raises ImportError: cannot import name 'JointType_FIXED' from 'tesseract_robotics.tesseract_scene_graph'; reading one as an attribute raises AttributeError.

before after
JointType_FIXED (also _UNKNOWN, _REVOLUTE, _CONTINUOUS, _PRISMATIC, _FLOATING, _PLANAR) JointType.FIXED (.UNKNOWN, .REVOLUTE, …)
from tesseract_robotics.tesseract_scene_graph import JointType_FIXED from tesseract_robotics.tesseract_scene_graph import JointType

Value types compare by value and are unhashable

Every collision, common and environment class whose C++ header declares operator== was bound without it, so == compared identity: ContactRequest() == ContactRequest() was False, and so was env == env.clone(). These classes now bind __eq__/__ne__ from the C++ operators and set __hash__ = None (#169):

module classes
tesseract_collision ContactResult, ContactResultMap, ContactRequest, ContactManagerConfig, CollisionCheckConfig
tesseract_common AllowedCollisionMatrix, BytesResource, CollisionMarginData, CollisionMarginPairData, GeneralResourceLocator, JointState, KinematicLimits, KinematicsPluginInfo, ManipulatorInfo, PluginInfo, PluginInfoContainer, SimpleLocatedResource
tesseract_environment Environment, Command, AddKinematicsInformationCommand, AddLinkCommand, AddSceneGraphCommand, ChangeCollisionMarginsCommand, ChangeJointAccelerationLimitsCommand, ChangeJointOriginCommand, ChangeJointPositionLimitsCommand, ChangeJointVelocityLimitsCommand, ChangeLinkCollisionEnabledCommand, ChangeLinkOriginCommand, ChangeLinkVisibilityCommand, ModifyAllowedCollisionsCommand, MoveJointCommand, MoveLinkCommand, RemoveAllowedCollisionLinkCommand, RemoveJointCommand, RemoveLinkCommand, ReplaceJointCommand

They join the classes that already compared by value (JointTrajectory, the plugin-info structs, AddTrajectoryLinkCommand, AddContactManagersPluginInfoCommand and the two SetActive*ContactManagerCommands).

What breaks is hashing. These objects are mutable, and an object that compares by value must not hash by identity, or two equal objects land in different set buckets. So hash(obj), {obj} and d[obj] = … now raise TypeError: unhashable type. Key on a field that identifies the object, or on id(obj) where identity is what you mean:

before after
seen = {cmd} seen = {(cmd.getType(), cmd.getLinkName())} (whatever names the command)
cache[env] = result cache[env.getName()] = result, or cache[id(env)] (keep env alive)
cmd in history meant identity any(c is cmd for c in history); in now compares values

The equality is the C++ one, unchanged, so it inherits upstream's choices:

  • Floating-point fields compare within a tolerance, not exactly: JointState vectors with isApprox(1e-5) after an exact size check (joint_state.cpp), ContactResult and CollisionCheckConfig with almostEqualRelativeAndAbs, the command origins with isApprox(1e-5).
  • ContactRequest.is_valid compares the validator object: two requests are equal only while they hold the same validator (types.cpp).
  • BytesResource and SimpleLocatedResource compare their parent locators only for presence: ResourceLocator::operator== returns true (resource_locator.cpp).
  • A geometry compares by type and its per-instance uuid (geometry.cpp), so commands holding geometry are equal only when they share it: the same URDF parsed twice gives unequal AddSceneGraphCommands.
  • Environment compares its revisions, the types of its commands, its current state and both timestamps (environment.cpp). env == env.clone() holds, because clone() copies the timestamps (L442–L444); two environments initialised separately from the same URDF are unequal. Its __eq__ releases the GIL, because the C++ operator takes the environment's lock.

Two classes keep identity equality: ResourceLocator and Resource. Their C++ operator== returns true for any pair (L46, L206), so binding it would make every two Python locators equal. A Python locator subclass stays hashable.

KinematicLimits compares the shapes of its four limit matrices before calling the C++ operator, which calls isApprox without checking them and reads out of bounds when they differ (kinematic_limits.cpp). Limits of different shapes are unequal.

createTaskComposerNode and createTaskComposerExecutor raise

Upstream's TaskComposerPluginFactory returns null when it cannot create a node or executor: the name is not configured, the plugin symbol fails to load, or the plugin factory throws. It only logs a console_bridge warning. Python got None, against a stub that promised the object. Both methods now raise TaskComposerPluginError, a RuntimeError (#203). For a name that is not configured, the message lists the configured names.

before after
node = factory.createTaskComposerNode(name)
if node is None: …
try: node = factory.createTaskComposerNode(name)
except TaskComposerPluginError: …
executor = factory.createTaskComposerExecutor(name)
if executor is None: …
try: executor = factory.createTaskComposerExecutor(name)
except TaskComposerPluginError: …

TaskComposer.plan still returns a failed PlanningResult for an unknown pipeline; its message is now the factory's error.

Pre-0.33 collision-margin aliases removed

tesseract 0.33 renamed the collision-margin members; the bindings kept the old names as aliases of the new ones. The eight aliases are gone (#177); each replacement takes the same arguments and does the same thing.

before after
config.margin_data_override_type config.pair_margin_override_type
manager.setDefaultCollisionMarginData(m) manager.setDefaultCollisionMargin(m)
manager.setPairCollisionMarginData(a, b, m) manager.setCollisionMarginPair(a, b, m)
CollisionMarginOverrideType CollisionMarginPairOverrideType
margins.getPairCollisionMargin(a, b) margins.getCollisionMargin(a, b)
margins.setPairCollisionMargin(a, b, m) margins.setCollisionMargin(a, b, m)

manager is a DiscreteContactManager or ContinuousContactManager, config a ContactManagerConfig, margins a CollisionMarginData. A ContactManagerConfig has no margin_data member (the collision API page used one; that snippet never ran): set its default_margin, pair_margin_data and pair_margin_override_type, then pass it to manager.applyContactManagerConfig(config).

SWIG-era enum constants removed

tesseract_collision, tesseract_environment and tesseract_common no longer export a module constant per enum value. The 14 constants were copies kept from the SWIG bindings; C++ has none of them, and every enum is bound as a class whose members are the same objects (#176). Importing one now raises ImportError: cannot import name 'ContactTestType_ALL' from 'tesseract_robotics.tesseract_collision'; reading one as an attribute raises AttributeError.

before after
ContactTestType_ALL (also _FIRST, _CLOSEST, _LIMITED) ContactTestType.ALL (.FIRST, .CLOSEST, .LIMITED)
Events_COMMAND_APPLIED, Events_SCENE_STATE_CHANGED Events.COMMAND_APPLIED, Events.SCENE_STATE_CHANGED
ModifyAllowedCollisionsType_ADD (also _REMOVE, _REPLACE) ModifyAllowedCollisionsType.ADD (.REMOVE, .REPLACE)
CONSOLE_BRIDGE_LOG_DEBUG (also _INFO, _WARN, _ERROR, _NONE) LogLevel.CONSOLE_BRIDGE_LOG_DEBUG (…)
Python
# before
from tesseract_robotics.tesseract_collision import ContactRequest, ContactTestType_ALL
from tesseract_robotics.tesseract_common import CONSOLE_BRIDGE_LOG_WARN, setLogLevel

request = ContactRequest(ContactTestType_ALL)
setLogLevel(CONSOLE_BRIDGE_LOG_WARN)

# after
from tesseract_robotics.tesseract_collision import ContactRequest, ContactTestType
from tesseract_robotics.tesseract_common import LogLevel, setLogLevel

request = ContactRequest(ContactTestType.ALL)
setLogLevel(LogLevel.CONSOLE_BRIDGE_LOG_WARN)

The CONSOLE_BRIDGE_LOG_* names keep their prefix because they are the C++ enumerator names of console_bridge's unscoped LogLevel; only the module-level copies are gone.

AnyPoly helpers only in tesseract_task_composer

tesseract_environment re-exported AnyPoly_wrap_EnvironmentConst, and tesseract_command_language re-exported AnyPoly_wrap_CompositeInstruction, AnyPoly_wrap_ProfileDictionary and AnyPoly_as_CompositeInstruction, from tesseract_task_composer inside a try: … except ImportError: pass. An install whose task-composer extension failed to load lost the names without an error, and importing either package loaded the task composer. The re-exports and tesseract_command_language._HAS_TASK_COMPOSER are removed (#192); the functions are defined in, and imported from, tesseract_task_composer. Code that imports them from the old place now fails with ImportError: cannot import name ….

before after
from tesseract_robotics.tesseract_environment import AnyPoly_wrap_EnvironmentConst from tesseract_robotics.tesseract_task_composer import AnyPoly_wrap_EnvironmentConst
from tesseract_robotics.tesseract_command_language import AnyPoly_wrap_CompositeInstruction (also AnyPoly_wrap_ProfileDictionary, AnyPoly_as_CompositeInstruction) from tesseract_robotics.tesseract_task_composer import AnyPoly_wrap_CompositeInstruction (…)

A str is content, pathlib.Path is a file

tesseract_common.FilesystemPath was a str subclass, so Environment.init had to guess whether a string was a path or URDF content, and init(str, str, locator) loaded files where the C++ std::string overload parses content. std::filesystem::path now converts with nanobind's caster (str | os.PathLike in, pathlib.Path out), and Environment.init binds the four native overloads (#165):

call meaning
init(urdf_xml, locator), init(urdf_xml, srdf_xml, locator) parse URDF/SRDF content
init(Path(urdf), locator), init(Path(urdf), Path(srdf), locator) load files
init(str(urdf_path), …) parsed as XML: returns False
init(urdf_xml, Path(srdf), locator) (either order) TypeError

The path overloads of Environment.init and the ContactManagersPluginFactory / KinematicsPluginFactory config constructors accept os.PathLike only, never str, because each sits beside a str content overload. Every other std::filesystem::path parameter takes str | os.PathLike.

before after
env.init(FilesystemPath(u), FilesystemPath(s), loc) env.init(Path(u), Path(s), loc)
env.init(path_str, loc) (path) env.init(Path(path_str), loc)
env.initFromUrdf(xml, loc) env.init(xml, loc)
env.initFromUrdfSrdf(u_xml, s_xml, loc) env.init(u_xml, s_xml, loc)
KinematicsPluginFactory(_FilesystemPath(p), loc) (also ContactManagersPluginFactory) KinematicsPluginFactory(Path(p), loc)
TaskComposerPluginFactory(FilesystemPath(p), loc) TaskComposerPluginFactory(str(p), loc)
TransformMap() {}

TaskComposerPluginFactory still takes its config path as str; its path/content pair is not bound yet.

The convex evaluators take the QP solution vector

QPProblem.evaluateConvexCosts, evaluateTotalConvexCost and evaluateConvexConstraintViolations read var_vals in the layout of the QP that the last convexify() built: getNumQPVars() entries, the NLP variables followed by the slack variables (L28). trajopt documents that size (L59, L67) but does not check it. The binding now raises ValueError for any other size, and before the first convexify(), while getNumQPVars() is still 0 (L99, L831):

Text Only
ValueError: evaluateConvexCosts: var_vals has 3 entries; it must be the QP solution vector of getNumQPVars() = 5 entries, the NLP variables followed by the slack variables
ValueError: evaluateConvexCosts: the problem has no convex model yet; call convexify() first

What used to work and now raises: an NLP-sized var_vals on a problem with constraint sets and no hinge or absolute cost. Each constraint row adds one or two slack variables, and trajopt 0.35.0, which every tesseract-robotics-nanobind 0.35.0.x wheel bundles, reads only the NLP block there, so the call returned the right values. Append zeros for the slack variables:

Python
problem.convexify()
n_slack = problem.getNumQPVars() - problem.getNumNLPVars()
model_costs = problem.evaluateConvexCosts(np.concatenate([x, np.zeros(n_slack)]))

A QP solution, such as SQPResults.new_var_vals in a callback, already has the right size. SQPResults.best_var_vals needs it too: on trajopt 0.35.0 it is NLP-sized until the first accepted step and QP-sized after it (always NLP-sized in a wheel built on a trajopt that contains trajopt#592), so take its first getNumNLPVars() entries and pad them as above. A problem with no constraint sets and only squared costs has no slack variables, so its NLP point is a QP point and nothing changes.

Why the binding checks: with a hinge or absolute cost, trajopt 0.35.0 multiplies the cost's full QP rows, slack columns included, into var_vals (L187–L188), so an NLP-sized vector was read past its end. On a one-joint problem whose absolute cost is 0.5, a view of the first three entries of a longer buffer read 3.5 or 0.5, depending on the two entries after the view, and a fresh three-entry array read 14.6. Before the first convexify(), an empty or NLP-sized vector segfaulted on a squared cost. The rule covers all three evaluators, although trajopt 0.35.0 reads only the NLP block in two of them: one contract, the one trajopt documents for all three once trajopt#592 is in.

IfoptQPProblem and IfoptProblem removed

tesseract_robotics.trajopt_sqp no longer binds IfoptQPProblem or IfoptProblem. trajopt is removing IfoptQPProblem (trajopt#595), and IfoptProblem was bound only to construct it. Code that uses either now fails with AttributeError: module 'tesseract_robotics.trajopt_sqp' has no attribute 'IfoptQPProblem' (or 'IfoptProblem'). Build a TrajOptQPProblem over the same variables, and add every set to it, each cost with a penalty type (#148):

Python
# before
nlp = tsqp.IfoptProblem(nodes_variables)
nlp.addConstraintSet(joint_constraint)
nlp.addCostSet(vel_cost)                        # no penalty type
problem = tsqp.IfoptQPProblem(nlp)
problem.addConstraintSet(collision_constraint)
problem.setup()

# after
problem = tsqp.TrajOptQPProblem(nodes_variables)
problem.addConstraintSet(joint_constraint)
problem.addConstraintSet(collision_constraint)
problem.addCostSet(vel_cost, tsqp.CostPenaltyType.SQUARED)
problem.setup()

It is not a rename. In trajopt 0.35.0, which every tesseract-robotics-nanobind 0.35.0.x wheel bundles (TrajOptQPProblem first ships in 0.35.0.9; earlier releases bind IfoptQPProblem), the two problems differ in five ways, and the third and fourth change results:

IfoptQPProblem (removed) TrajOptQPProblem
1. Per set, not per row One merit coefficient, name and violation per constraint row; names are <set>_<row> (L93, L100). One per constraint set (L668, L675, L1021–L1043). Code that indexes names, violations or merit coefficients by row breaks, and the solver inflates a whole set's merit coefficient at once.
2. addCostSet checks bounds Checks nothing; rejects HINGE (L50–L78). SQUARED and ABSOLUTE need equality bounds on every row, HINGE one-sided bounds; anything else, a range row included, raises (L416–L476).
3. No raw-cost path A cost added with IfoptProblem.addCostSet bypasses IfoptQPProblem::addCostSet (L59): it gets no gradient and no Hessian in the QP (L190, L263), while the merit still reads its raw, signed rows (L525–L531). Every cost goes in with addCostSet(set, penalty_type). A squared cost that was inert now shapes the steps; for ABSOLUTE and HINGE costs on trajopt 0.35.0, see the fourth difference.
4. The merit matches the model for squared costs Weights the model but not the merit, and counts the model once per cost term, so the solver can reject every step and stop at the seed (trajopt#595). The merit weights and counts squared costs as the model does, so steps the old ratio rejected can be accepted. trajopt 0.35.0 has two exceptions, both fixed by trajopt#592 (merged 2026-09-30; the fix arrives only with a wheel built on a newer trajopt, none yet). Constraint coefficients weight a set's slack in the QP (L798) but not its violation in the merit (L1021–L1043). The model reads an ABSOLUTE or HINGE cost as 0 at every QP solution (L166–L196) while its exact cost ignores its coefficient (L1015), so the model predicts the whole cost away and the solver rejects a step that removes less than improve_ratio_threshold of it; a violation the trust box cannot cut by that much stops at its seed (the QP problem).
5. Dynamic sets Throws on isDynamic() sets (L42–L48). Accepts them (L404–L414).

How much the third difference moves a result: this package's online SQP example added its velocity cost with IfoptProblem.addCostSet, so its QP had no cost term and OSQP solved with an empty Hessian. As a squared cost on TrajOptQPProblem, the global solve's summed squared joint steps fell from 23.2 to 3.9, against 3.6 for the straight-line seed, and getTotalExactCost() now reports that sum instead of the first velocity row (0.5).

Evidence

The five differences are read from the trajopt 0.35.0 sources linked above. The example's numbers are measured on this repository's online_planning_sqp_example.py, and tests/examples/test_online_planning_sqp_example.py pins its problem class, its cost as the squared joint steps, and a global solve that reaches the target from the pinned start. How much results move in other code depends on its costs; nothing here measures that. The ABSOLUTE/HINGE exception is also measured: TestTrajOptQPProblemPenaltyCosts in tests/trajopt_sqp/test_trajopt_sqp_bindings.py pins it.

0.35.0.1

Transform removed; Pose is an Isometry3d

tesseract_robotics.planning.Pose now subclasses Isometry3d. Its rotation math runs in Eigen, its numpy-property facade is gone, its stored data are properties, and its factories collapse to one name each that accepts both scalar-positional and array-like arguments. The Transform alias is removed: use Pose (#76).

Resource URIs moved

tesseract 0.35 folds its support data into the tesseract package, so package://tesseract_support/... is now package://tesseract/support/... in every URDF, SRDF and locateResource call. The rest of that upgrade leaves the Python API unchanged; see the 0.34 → 0.35 guide.

0.34.1.0

The tesseract 0.34 upgrade removed the ifopt module (its types moved to trajopt_ifopt) and replaced JointPosition with Var / Node / NodesVariables. It also removed CartPosInfo and CollisionCache, dropped getStaticKey() from profiles, and renamed the cost and constraint evaluation methods. The 0.33 → 0.34 guide has each change with its replacement.