Skip to content

Changelog

Unreleased

Breaking changes

  • CollisionCoeffData.setPairCollisionCoeff / getPairCollisionCoeff removed — copies of setCollisionCoeff / getCollisionCoeff under names C++ does not have. Call the C++ names (#298).
  • CollisionCoeffData, TrajOptCollisionConfig and SQPParameters compare by value and are unhashable — == compared identity; hash(), set members and dict keys now raise TypeError (#261).
  • The collision constraints reject a Var outside a variable set — DiscreteCollisionConstraint, DiscreteCollisionNumericalConstraint, ContinuousCollisionConstraint and the new collision classes raise VarNotInVariableSetError (a ValueError) at construction when the Var is not in a NodesVariables. trajopt reads the set's hash through the Var's parent pointers unchecked, so such a constraint dereferenced null on its first update(). A Var from Node.addVar qualifies once its Node is handed to NodesVariables (#297).
  • OSQPEigenSolver's update methods check sizes — updateGradient, updateLowerBound, updateUpperBound and updateBounds raise QPSizeError (a ValueError) on a vector whose length is not the init() size; trajopt indexed past it (#283).
  • Command-language types compare by value and are unhashable — the 16 waypoint, instruction and Poly types bind __eq__/__ne__ from the C++ operators and set __hash__ = None; hash(), set members and dict keys raise TypeError. MoveInstruction equality includes both UUIDs (#261; docs/breaking-changes.md has the migration).
  • Time-parameterization profiles compare by value and are unhashable — TOTGCompositeProfile, ISPCompositeProfile, ISPMoveProfile and ConstantTCPSpeedCompositeProfile bind __eq__/__ne__ from the C++ operator== (identity before) and set __hash__ = None, as #169 did for collision, common and environment: hash(), set members and dict keys raise TypeError (#261).
  • The seven simple-planner move profiles compare by value and are unhashable — SimplePlannerFixedSize{,Assign,AssignNoIK}MoveProfile and SimplePlannerLVS{,NoIK,Assign,AssignNoIK}MoveProfile bind the C++ operator==; hash(), set members and dict keys raise TypeError (#261).

  • OMPLSolverConfig.getNumPlanners() removed; planners is readable — solver_config.planners binds the configurator list, so Python sees which planners a profile holds, not only how many. The list is copied on access; its configurators are shared with the config, so cfg.planners[0].range = 0.5 edits the config. To change which planners it holds, assign a new list, or use addPlanner / clearPlanners, which stay as the in-place mutators (#302).

before after
cfg.getNumPlanners() len(cfg.planners)
  • OMPL, TrajOpt and task-composer planning value types compare by value and are unhashable — OMPLSolverConfig, OMPLRealVectorMoveProfile, TrajOptCartesianWaypointConfig, TrajOptJointWaypointConfig, TrajOptDefaultMoveProfile, TrajOptDefaultCompositeProfile, TrajOptOSQPSolverProfile, BasicTrustRegionSQPParameters and the seven tesseract_task_composer_planning profiles bind the C++ operator==; hash(), set members and dict keys raise TypeError. OMPLSolverConfig compares planners by count only, as upstream (#261).
  • create_ompl_planner_configurators rejects a keyword that fits none of the requested planners — it accepts every OMPLPlannerType name and sets each keyword on the configurators that have that field; a keyword no requested configurator has raises ValueError instead of being ignored (#301).
  • TrajOpt-Ifopt and Descartes profiles and waypoint configs compare by value and are unhashable — TrajOptIfoptCartesianWaypointConfig, TrajOptIfoptJointWaypointConfig, TrajOptIfoptDefaultMoveProfile, TrajOptIfoptDefaultCompositeProfile, TrajOptIfoptOSQPSolverProfile, DescartesDefaultMoveProfileD and DescartesLadderGraphSolverProfileD bind __eq__/__ne__ from the C++ operator== and set __hash__ = None; hash(), set members and dict keys raise TypeError (#261).

Changes

  • Motion-planner core: the rest of utils.h, request reasons, robot configuration — toToolpath (three overloads), formatProgram and contactCheckProgram (discrete and continuous; returns (ContactTrajectoryResults, list[ContactResultMap]) with the GIL released) (#303); MotionPlanner.checkRequestReason(request) -> str and MotionPlanner.assignSolution (#332); RobotConfig, getRobotConfig, getJointTurns, isWithinJointLimits (#333; isValidState<double> does not compile upstream and stays unbound). The module imports the modules that define Environment, CompositeInstruction and ProfileDictionary, so its stub names them instead of quoting C++ names (#300).
  • Simple planner — SimpleMotionPlanner derives from MotionPlanner (#299); interpolate (poses and joints), interpolate_waypoint and getInterpolatedInstructions (#305); SimplePlannerMoveProfile.generate (#339); SimplePlannerMoveProfile.createKey() / SimplePlannerCompositeProfile.createKey(), inherited by every concrete profile, return the key the profile registers under (#273); from_yaml(text) on the seven move profiles (#331).
  • All 14 OMPL planner configurators — ESTConfigurator, LBKPIECE1Configurator, BKPIECE1Configurator, KPIECE1Configurator, BiTRRTConfigurator, RRTConfigurator, TRRTConfigurator, PRMConfigurator, PRMstarConfigurator, LazyPRMstarConfigurator and SPARSConfigurator join the three already bound, one per OMPLPlannerType, with every public field. create stays unbound: it takes OMPL types (#301).
  • OSQPSettings and TrajOptOSQPSolverProfile.settings — the classic TrajOpt planner's QP solver is tunable from Python: tolerances, iteration and time limits, polishing, adaptive rho. OSQPSettings() holds OSQP's defaults, a new profile trajopt's. The OSQP enum fields linsys_solver and cg_precond are not bound (#306).
  • TrajOptDefaultCompositeProfile.computeLongestValidSegmentLength — the static helper that turns a longest-valid-segment fraction and length into the collision-check step (#307).
  • FixStateCollisionProfile binds its TrajOpt correction fields — trajopt_joint_constraint_config, trajopt_joint_cost_config, collision_constraint_coeff, collision_cost_coeff, opt_params and osqp_settings; tesseract_task_composer_planning now imports tesseract_motion_planners_trajopt and trajopt_ifopt at load (#326).
  • OMPLMotionPlanner and TrajOptMotionPlanner derive from MotionPlanner — isinstance(planner, MotionPlanner) holds, and MotionPlanner.solve(planner, request) dispatches to the planner (#299).
  • Profile hooks OMPLMoveProfile.createSolverConfig and TrajOptSolverProfile.createOptimizationParameters — Python can read what the planner builds from a profile (#339).
  • Static createKey() on the OMPL, TrajOpt and task-composer planning profiles — returns the key ProfileDictionary uses, without building a profile. The OMPL and TrajOpt keys belong to the profile kind (OMPLMoveProfile, TrajOptMoveProfile, TrajOptCompositeProfile, TrajOptSolverProfile) and are inherited by the concrete profiles (#273).
  • from_yaml on the OMPL, TrajOpt and task-composer planning profiles — OMPLRealVectorMoveProfile, TrajOptDefaultMoveProfile, TrajOptDefaultCompositeProfile, TrajOptOSQPSolverProfile and the seven tesseract_task_composer_planning profiles load from YAML text through the C++ YAML constructor. A key replaces the whole sub-config it names, starting from that config's own defaults (collision_check_config: {...} resets type to DISCRETE; solver_config: {...} starts with no planners). Malformed YAML raises ValueError, a value of the wrong type the profile's RuntimeError (#331).
  • TrajOpt-Ifopt and Descartes planners are MotionPlanners — TrajOptIfoptMotionPlanner and DescartesMotionPlannerD derive from tesseract_motion_planners.MotionPlanner; getName/solve/terminate/clear/clone are the base's (its solve releases the GIL) (#299).
  • The Descartes stub names Environment — the binding imports tesseract_environment, so DescartesMoveProfileD.create* no longer quote "tesseract::environment::Environment" (#300).
  • Python SQP callbacks reach the TrajOpt-Ifopt planner — TrajOptIfoptSolverProfile.callbacks (copied on access; None rejected) and createOptimizationCallbacks(). On trajopt 0.35.0 a callback returning False does not stop the solve (upstream stepSQPSolver ignores the stopped status) (#308).
  • The TrajOpt-Ifopt term factories — createJointPositionConstraint, createCollisionConstraints, createJointVelocityConstraint, createJointAccelerationConstraint, createJointJerkConstraint from trajopt_ifopt_utils.h; an empty or None-holding vars raises (#309).
  • DescartesStateSampleD(state) and str() on Descartes waypoint samplers — the one-argument constructor (cost 0) and __str__ from upstream's print (#310).
  • Descartes tool-pose samplers — sampleFixed, sampleToolAxis, sampleToolXAxis, sampleToolYAxis, sampleToolZAxis, returning tesseract_common.VectorIsometry3d (#311).
  • TrajOpt-Ifopt profile hooks — TrajOptIfoptMoveProfile.create, TrajOptIfoptCompositeProfile.create and TrajOptIfoptSolverProfile.create, with the structs they return: TrajOptIfoptTermInfos and the move-only TrajOptIfoptWaypointInfo (its node read-only and keeping the info alive) (#339).
  • createKey() on the TrajOpt-Ifopt and Descartes profile bases — TrajOptIfopt{Move,Composite,Solver}Profile.createKey() and Descartes{Move,Solver}ProfileD.createKey() return the key every profile of that kind carries; subclasses inherit it (#273).
  • TrajOpt-Ifopt profiles load from YAML text — TrajOptIfoptDefaultMoveProfile.from_yaml, TrajOptIfoptDefaultCompositeProfile.from_yaml and TrajOptIfoptOSQPSolverProfile.from_yaml(config) run the C++ YAML constructor; malformed YAML raises ValueError, a value of the wrong type the constructor's RuntimeError (#331).
  • Python-authored simple-planner move profiles — subclass SimplePlannerMoveProfile and override generate; its instruction and manipulator-info arguments arrive as copies, so an override may keep them. A generate that returns an empty list or a null instruction raises SimplePlannerEmptyGenerateError / SimplePlannerNullInstructionError (both ValueError; upstream would read past the end or plan the null as a success). super().generate(...) from such an override raises SimplePlannerPureVirtualCallError (a NotImplementedError; the base is pure virtual). An exception generate raises propagates from SimpleMotionPlanner.solve with its type when the plan fails (the last one raised wins; upstream turns it into a failed response, and the MotionPlanner base solve and the task composer still do). A profile without generate raises SimplePlannerPureVirtualCallError from solve, a generate returning anything but a list of MoveInstructionPoly raises SimplePlannerProfileReturnTypeError (a TypeError naming the offending element's type), and one whose last instruction differs from base_instruction in move type, waypoint type, profile or path profile raises SimplePlannerGenerateMismatchError (a ValueError; upstream asserts this in debug builds only). The helpers a C++ override calls are bound: JointGroupInstructionInfo / KinematicGroupInstructionInfo (each keeps its instruction and environment alive), the twelve interpolate{Joint,Cart}{Joint,Cart}Waypoint overloads and both getClosestJointSolution overloads (the pair one returns a tuple). JointGroupInstructionInfo.getWorkingFrame is declared upstream but never defined and stays unbound (#340).
  • DescartesMoveProfileD.createKinematicGroup(manip_info, env) — the helper a C++ Descartes override calls; the returned KinematicGroup keeps env alive, whose kinematics plugins hold its IK solver code (#340).
  • Python-authored TrajOpt-Ifopt move and composite profiles — subclass TrajOptIfoptMoveProfile or TrajOptIfoptCompositeProfile and override create; a composite profile can add Python ConstraintSets to the terms it returns, and those sets are released safely on a task-composer worker thread. An exception create raises propagates from solve with its type. super().create(...) from such an override, or a subclass without create, raises TrajOptIfoptPureVirtualCallError (a NotImplementedError). A move profile's create must return an info that has a node (TrajOptIfoptWaypointInfoWithoutNodeError) and that nothing else references, since the planner takes its node (TrajOptIfoptWaypointInfoInUseError, decided by the reference count, which is exact on the GIL builds shipped); any other return type raises TrajOptIfoptProfileReturnTypeError. A composite create that changes the planner's (read-only) nodes raises TrajOptIfoptNodeLayoutChangedError: added variables would change the problem's layout, and changed values would be silently overwritten by the planner's seed. TrajOptIfoptSolverProfile.create stays non-overridable: it returns the concrete TrustRegionSQPSolver (#340).
  • Overriding a hook on a concrete profile raises — only the abstract bases forward virtual calls to Python, so a generate / create / create* override on a Python subclass of a concrete profile (the seven simple-planner move profiles, TrajOptIfoptDefault{Move,Composite}Profile, TrajOptIfoptOSQPSolverProfile, DescartesDefaultMoveProfileD, DescartesLadderGraphSolverProfileD) was silently never called. Such a class statement now raises ProfileOverrideNotDispatchedError (a TypeError, exported by each planner module), naming the abstract base to subclass instead (#340).
  • Docs build with Zensical — the site builds with zensical build --strict instead of MkDocs + Material for MkDocs (maintenance ends 5 November 2026; MkDocs 2.0 drops plugins). mkdocs.yml is read unchanged and renders the same 54 pages; strict mode now also fails on broken anchors. Zensical and mkdocstrings>=1 need Python ≥3.11, so the docs tooling moves out of the default pixi feature into a docs env (Python 3.12, no C++ stack): pixi run docs / pixi run docs-build run there (#252).
  • Command-language bindings complete — instruction UUIDs as canonical strings (getUUID/setUUID/getParentUUID/setParentUUID/regenerateUUID on the nine instruction types, #262); copy.copy/copy.deepcopy on the waypoints, instructions and Polys (#263); WaypointPoly, MoveInstructionPoly and InstructionPoly built from the types they wrap, the full MoveInstruction (path profile, manipulator_info) and five-argument StateWaypoint constructors (#264); profile overrides (#265); createChild/isChild, create*Waypoint, isLinear/isFreespace/isCircular (#266); tolerances, setSeed, effort on the waypoint Polys (#267); as* casts returning references on the waypoint and move-instruction Polys (#268); CompositeInstruction insert, __delitem__, pop_back, front/back/at and capacity members (#269), traversal (flatten, getFirst/Last(Move)Instruction, counts, moveFilter, #270) and getUserData/setUserData (#271); ProfileDictionary.addProfile for several names, getProfileEntry, getAllProfileEntries (#272); the utils.h functions (toJointTrajectory, joint-position helpers, clampToJointLimits, makeTimeMonotonicallyIncreasing, toDelimitedFile, #274; validateSeedStructure is not bound: declared in utils.h, not defined in tesseract 0.35.0).
  • The binding audit states its coverage — the report names every binding module it does not audit, with the reason; a binding module in neither table fails the run. Header ownership supports several prefixes per module and nested prefixes; the planning and trajopt bindings now parse (#256).
  • trajopt_ifopt names its tesseract types — the extension imports tesseract_kinematics and tesseract_environment, so its signatures and stub name JointGroup, KinematicGroup and Environment instead of quoted C++ names (#285).
  • The planner's collision constraints — DiscreteCollisionConstraintD, ContinuousCollisionConstraintD (one row per contact) and ContinuousCollisionNumericalConstraint are bound; trajopt_ifopt and trajopt_sqp drop __all__, so the star import re-exports every bound name, toString included (#291, #284).
  • Collision evaluator queries — calcCollisions / calcCollisionData return a CollisionCacheData; getGradient, calcGradientData and getJointGroup are bound on the two evaluator bases, with the trajopt_common result structs (LinkGradientResults, GradientResults, LinkMaxError, GradientResultsSet, CollisionCacheData) (#292).
  • Bounds += s / Bounds -= s shift both limits and reclassify the bounds type; the name keeps the Bounds (#286).
  • Component.getValues / getBounds, Differentiable.getJacobian and Differentiable.Mode are bound on the bases (#287).
  • Constraint accessors — setBounds on every constraint that declares it (raises BoundsSizeError unless one bound per row), CartLineConstraint.getLine / getInfo, InverseKinematicsConstraint.setTargetPose / getKinematicInfo (#288).
  • calcJacobianBlock(joint_vals) on CartPosConstraint, CartLineConstraint and DiscreteCollisionNumericalConstraint: the Jacobian at given joint values, sized like getJacobian(); raises JointValuesSizeError on a wrong length. InverseKinematicsConstraint.calcJacobianBlock stays unbound: trajopt 0.35.0 declares it but does not define it (#289).
  • CartLineInfo(...) / InverseKinematicsInfo(...) field constructors, with the C++ defaults as overloads. The list[Bounds] constructors of JointVel / JointAccel / JointJerkConstraint stay unbound: trajopt 0.35.0 declares them but does not define them (#290).
  • CollisionCoeffData.getCollisionCoeffPairData / getPairsWithZeroCoeff (#293).
  • getClosestValidPoint, calcBoundsErrors, calcBoundsViolations (#294).
  • calcNumericalConstraintGradient(variables, constraint_set) and calcForwardNumJac(f, x, epsilon) check an analytic Jacobian, a Python ConstraintSet's included (#295).
  • toTrajArray(vars) / toJointTrajectory(vars); the online SQP example reads its warm start with toTrajArray (#296).
  • NodesVariables(name, nodes) and the Var constructors — Python builds variable sets with several vars or per-node bounds; the Nodes hand over ownership. Var.getParent / Node.getParent (#297).
  • QPProblem convex model and setVariables — getHessian, getGradient, getConstraintMatrix, getBoundsLower, getBoundsUpper, getConstraintMeritCoeff, and setVariables(x), which raises VariablesSizeError unless len(x) == getNumNLPVars() (#282).
  • QPSolver update interface — updateHessianMatrix, updateGradient, updateLowerBound, updateUpperBound, updateBounds, updateLinearConstraintsMatrix and setWarmStart(problem) on the base; OSQPEigenSolver.setWarmStart takes a bool (the OSQP option) or a QPProblem (#283).

Fixed

  • A Var or Node read out of a variable set keeps the set alive — Node.getVar / addVar, NodesVariables.getNode / getNodes and Var.getParent tie the result to its source, and a Node handed to NodesVariables keeps the set alive; var.getParent() after the set was dropped read freed memory (#297).
  • NodesVariables.getNode / getNodes return usable Nodes while the caller still holds the handed-over ones — they returned the caller's relinquished object, on which every method raised TypeError. A Node given twice, or one already in a set, raises NodeOwnershipError (a ValueError) and leaves every Node with the caller (#297).
  • Linking a constraint to a variable set that does not hold its Vars raises VarNotInVariableSetError — from linkWithVariables and from TrajOptQPProblem.addConstraintSet / addCostSet; a standalone Var or one from another set put the Jacobian's columns out of range (#297).
  • Node.getParent no longer reads a freed variable set — it returned a raw pointer to the Node's set, which a planner frees after its solve while a Python composite profile's create may still hold the Node. It now returns a set Python built (NodesVariables(...), createNodesVariables) only while that set is alive and holds the Node, and keeps it alive; otherwise it raises NodeParentUnavailableError (a RuntimeError). Linking a constraint checks that the set holds its Vars instead of comparing addresses, so a Var whose set was freed cannot pass for a member of a new set at the reused address. An unlinked collision constraint built on a Var kept from a planner's set raises the same error from update() instead of reading the freed set; one on a set built from Python updates as before. A Python subclass of NodesVariables is its own getParent() result, and NodesVariables(name, "...") with a str for nodes raises a clear TypeError (#297).
  • calcForwardNumJac raises NumericalJacobianSizeError (a ValueError) when f returns a different length at a perturbed x; trajopt read past the shorter vector (#295).
  • CartPosConstraint.calcJacobianBlock takes its joint count from its Var — it divided getNonZeros() by getRows(), and accepted any length with every axis weighted zero (#289).
  • A stored SQPCallback problem can't outlive the call — a problem with no Python object (one TrajOptIfoptMotionPlanner builds in C++) reaches a Python callback as a QPProblemView; once the call returns, every method on it raises QPProblemExpiredError (a RuntimeError). A callback that kept the problem used to read freed memory after solve() returned. A problem passed to solve() from Python still arrives as that object (#308).
  • InstructionsTrajectory(program) keeps program alive — it stores references into the program; built from a temporary (InstructionsTrajectory(make_program())) it read freed memory once the program was collected. It also holds references into the program's instruction vector, which appending, inserting or erasing instructions can reallocate: a later call returned stale values (5 appends), raised a RuntimeError (50) or crashed (20000). The binding now records the addresses of the program's move instructions and re-flattens the program before every method; on any difference it raises the new StaleTrajectoryError (a RuntimeError): build a new trajectory. setData still writes into the program. Cost per call: +0.3 µs on an 11-move program, +4.7 µs on 1000 moves. getPosition, getVelocity, getAcceleration, getTimeFromStart and setData raise IndexError for i outside [0, size()) (upstream indexes unchecked); negative i is rejected, not wrapped (#281).
  • TimeParameterization.compute names Environment in its signature — the stub said "tesseract::environment::Environment"; the module now imports tesseract_environment first (#277).
  • Ruckig jerk-limited smoothing — RuckigTrajectorySmoothing and RuckigTrajectorySmoothingCompositeProfile (duration_extension_fraction, max_duration_extension_factor, override_limits, velocity_limits, acceleration_limits, jerk_limits) are bound; the module links tesseract::time_parameterization_ruckig (#278).
  • rescaleTimings(program, scalings) — rescales each sub-composite's timing in place; a scalings list that is not len(program) long raises the new ScalingsLengthError (a ValueError; upstream only asserts) (#279).
  • ISPCompositeProfile.minimum_time_delta is bound (#280).
  • createKey() on the time-parameterization profiles — a static X.createKey() returns the ProfileDictionary key X().getKey() without building a profile, on all five profile classes (#273).

0.35.0.9 - 2026-10-15 — TrajOptQPProblem + value equality + named planning frames + binding audit

Breaking changes

  • The planning API no longer picks a TCP or a working frame — Robot.get_manipulator_info and TaskComposer.plan guessed the TCP as the last entry of getActiveLinkNames() (an order that holds for a serial chain, not a tree), MotionProgram and get_manipulator_info defaulted working_frame to the literal "base_link", and get_manipulator_info cached the guess and handed every caller the same mutable ManipulatorInfo. Robot.fk / Robot.ik guessed tip_link the same way. tcp_frame and working_frame are now required in MotionProgram and get_manipulator_info, tip_link in fk and ik; a missing one raises TypeError at the call. Both take a keyword-only tcp_offset (str | Isometry3d, default identity) that goes to ManipulatorInfo.tcp_offset unchanged, so a named SRDF TCP reaches the planner, which resolves it with Environment.findTCPOffset. get_manipulator_info builds a new object per call; to_composite_instruction loses its tcp_frame override, the program's frames are the only source (#30, #133).
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) (also tip_link= after seed) robot.ik("manipulator", pose, "tool0", seed=q)
program.to_composite_instruction(joint_names, "tool0") program.to_composite_instruction(joint_names)
ManipulatorInfo with a TCP offset, built by hand MotionProgram(..., tcp_offset="laser") or tcp_offset=Isometry3d(...)

For the support robots (abb_irb2400, lbr_iiwa_14_r820) the old guesses were "tool0" and "base_link": passing those reproduces the old behaviour exactly.

  • std::filesystem::path is pathlib.Path; Environment.init(str, …) always means content — tesseract_common.FilesystemPath was a str subclass, so Environment.init could not tell a path from URDF content: init(str, str, locator) treated both strings as paths (the opposite of the C++ std::string overload), init(str, locator) guessed from a leading <, and the Python-only initFromUrdf / initFromUrdfSrdf existed to work around the guess. std::filesystem::path now converts with nanobind's caster (str | os.PathLike in, pathlib.Path out), and Environment.init binds the four native overloads: a str is URDF/SRDF content, a pathlib.Path (any os.PathLike) is a file. Where a path overload sits beside a content overload (Environment.init ×2, the ContactManagersPluginFactory and KinematicsPluginFactory config constructors), the path parameter accepts os.PathLike only, so a str never reaches it and a mixed init(content, Path, locator) raises TypeError. Removed: FilesystemPath, _FilesystemPath, TransformMap (a plain dict converts) and Environment.initFromUrdf / initFromUrdfSrdf. A path left as str makes init return False; it does not raise (#165).
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: tesseract_task_composer is outside the audited modules, so its native path/content pair is not bound yet.

  • tesseract_environment and tesseract_command_language no longer re-export task-composer functions — tesseract_environment/__init__.py imported AnyPoly_wrap_EnvironmentConst from tesseract_task_composer inside try: … except ImportError: pass, and tesseract_command_language/__init__.py did the same for AnyPoly_wrap_CompositeInstruction, AnyPoly_wrap_ProfileDictionary and AnyPoly_as_CompositeInstruction, behind a _HAS_TASK_COMPOSER flag. A broken task-composer extension (a missing dylib, say) made the names vanish silently, and from tesseract_robotics.tesseract_environment import * then raised AttributeError for a name in __all__, far from the cause. Both imports also loaded the task composer as a side effect. The re-exports, their __all__ entries and _HAS_TASK_COMPOSER are removed; import the functions from tesseract_task_composer, where they are defined. Importing either package no longer imports tesseract_task_composer (#192).
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 (…)
tesseract_command_language._HAS_TASK_COMPOSER removed: the task composer is a hard dependency
  • SWIG-era enum constants removed — tesseract_collision, tesseract_environment and tesseract_common exported a module constant for each value of four bound enums, kept from the SWIG bindings: ContactTestType_FIRST / _CLOSEST / _ALL / _LIMITED, Events_COMMAND_APPLIED / _SCENE_STATE_CHANGED, ModifyAllowedCollisionsType_ADD / _REMOVE / _REPLACE and the module-level CONSOLE_BRIDGE_LOG_DEBUG / _INFO / _WARN / _ERROR / _NONE. C++ has no such constants (the CONSOLE_BRIDGE_LOG_* names are LogLevel enumerators, not module names), and each enum is bound as a class. All 14 constants and their __all__ entries are gone: importing one raises ImportError, reading one as a module attribute raises AttributeError. Use the enum member; the values are the same objects (#176).
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 (…_INFO, …_WARN, …_ERROR, …_NONE)
  • Pre-0.33 collision-margin aliases removed — eight Python names that forwarded to members tesseract 0.33 renamed, each bound next to the native member: ContactManagerConfig.margin_data_override_type, DiscreteContactManager / ContinuousContactManager .setDefaultCollisionMarginData and .setPairCollisionMarginData, tesseract_common.CollisionMarginOverrideType, and CollisionMarginData.getPairCollisionMargin / .setPairCollisionMargin. Using one raises AttributeError (ImportError for from tesseract_robotics.tesseract_common import CollisionMarginOverrideType). The ContactManagerConfig example in the collision API page used a config.margin_data member that never existed; it now sets default_margin, pair_margin_data and pair_margin_override_type (#177).
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)
  • createTaskComposerNode / createTaskComposerExecutor raise TaskComposerPluginError instead of returning None — upstream returns null for an unknown name, a plugin symbol that fails to load and a plugin factory that throws, with only a console_bridge warning on stderr, and the binding passed that on as None although the stub promised a TaskComposerNode / TaskComposerExecutor. Callers got an AttributeError later, far from the cause, and TaskComposer.warmup / get_available_pipelines counted an unknown or unbuildable name as loaded, since their except never fired. Both methods now raise the new TaskComposerPluginError (a RuntimeError): for a name that is not configured, the message lists the configured names; for a configured plugin that cannot load or build, it points at the warning. TaskComposer.warmup and get_available_pipelines catch it, and TaskComposer.plan returns its message in the failed PlanningResult (#203).
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: …
  • Value types compare by value and are unhashable — the collision, common and environment classes that declare operator== were bound without it, so == compared identity: two equal objects compared unequal and nothing raised. 37 classes now bind __eq__/__ne__ and set __hash__ = None: ContactResult, ContactResultMap, ContactRequest, ContactManagerConfig, CollisionCheckConfig; AllowedCollisionMatrix, BytesResource, CollisionMarginData, CollisionMarginPairData, GeneralResourceLocator, JointState, KinematicLimits, KinematicsPluginInfo, ManipulatorInfo, PluginInfo, PluginInfoContainer, SimpleLocatedResource; Environment, Command and all 18 remaining commands. hash() on them, and using one as a set member or dict key, now raises TypeError. Equality is upstream's: floating-point fields compare within the C++ tolerance (JointState positions with isApprox(1e-5), for example), ContactRequest.is_valid compares the validator object, BytesResource and SimpleLocatedResource check only whether a parent is set, geometry compares by its per-instance uuid (so the same URDF parsed twice gives unequal AddSceneGraphCommands), and Environment compares its timestamps, so two environments initialised separately are unequal while env == env.clone() holds. Environment.__eq__ releases the GIL, since the C++ operator takes the environment's lock. An operand of another class (MoveLinkCommand(j) == RemoveLinkCommand(l), cfg == 5) compares unequal without raising. ResourceLocator and Resource keep identity equality, because their C++ operator== returns true for any two objects. KinematicLimits checks the four matrix shapes before calling the C++ operator, which does not and would read out of bounds (#169).
before after
ContactRequest() == ContactRequest() → False True
env.clone() == env → False True (separately initialised: False, the timestamps differ)
seen = {cmd}, cache[env] = x TypeError: unhashable type; key on a stable field, e.g. {cmd.getType(): cmd}, cache[env.getName()], or id(env) for identity
cmd in history (identity) cmd in history (value); any(c is cmd for c in history) for identity
  • SWIG-era JointType_* constants removed — tesseract_scene_graph exported a module constant for each JointType value (JointType_FIXED, JointType_REVOLUTE, JointType_CONTINUOUS, JointType_PRISMATIC, JointType_FLOATING, JointType_PLANAR, JointType_UNKNOWN), kept from the SWIG bindings. C++ has no such names, and JointType is bound as an enum. All seven are gone: importing one raises ImportError, reading one as a module attribute raises AttributeError. Use the enum member; the values are the same objects. This finishes #176 for the scene graph module, which now joins its no-alias guard (#214).
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
  • PointCloudPoint renamed to PointCloud.Point — the point type of tesseract_geometry.PointCloud is the nested struct PointCloud::Point in C++, but was bound at module level as PointCloudPoint, a name C++ does not have. It is now bound nested, and PointCloudPoint and its __all__ entry are gone: importing it raises ImportError, reading it as a module attribute raises AttributeError. PointCloud.points is typed list[PointCloud.Point]. addPoint is unchanged and is still the way to grow a cloud (points converts to a fresh list on each access). Octree.update stays unbound: upstream's body is assert(false) (#209).
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
  • ChangeCollisionMarginsCommand(pair_margin_data) defaults to MODIFY, as upstream — the binding defaulted override_type to REPLACE, so leaving it out replaced every pair margin where the same C++ call merges the given pairs into the existing ones. Pass CollisionMarginPairOverrideType.REPLACE to keep the old behaviour (#247).
  • IfoptQPProblem and IfoptProblem removed; TrajOptQPProblem replaces them — code that builds IfoptQPProblem(IfoptProblem(nodes)) now fails with AttributeError: build TrajOptQPProblem(nodes) and add every constraint and cost set to it, each cost with its penalty type. It is not a rename, and results change: merit coefficients, names and violations are per set, not per row; addCostSet checks bounds; no cost bypasses the QP; the merit matches the model for squared costs; dynamic sets are accepted. docs/breaking-changes.md has the migration and the five differences (#148). trajopt is removing IfoptQPProblem (trajopt#595), and IfoptProblem was bound only to construct it. The online SQP example, the tests and the docs build TrajOptQPProblem, as tesseract_planning's planner and its online_planning_example.cpp do. The example added its velocity cost to the wrapped IfoptProblem, which kept it out of the QP (only IfoptQPProblem.addCostSet feeds the gradient and the Gauss-Newton Hessian) while the merit read it raw, so OSQP solved with no cost term and an empty Hessian: the global solve's squared joint steps summed to 23.2. As a squared cost they sum to 3.9, against 3.6 for the straight-line seed, and getTotalExactCost() reports that sum instead of the first velocity row.
  • The convex evaluators take the QP solution vector — evaluateConvexCosts, evaluateTotalConvexCost and evaluateConvexConstraintViolations raise ValueError unless var_vals has exactly getNumQPVars() entries, the NLP variables followed by the slack variables, and before the first convexify(). trajopt documents that size but does not check it, and trajopt 0.35.0, which every 0.35.0.x wheel bundles, multiplies a hinge or absolute cost's full QP rows into var_vals, so a shorter 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 entries after it, and a fresh three-entry array 14.6. Before the first convexify() an empty or NLP-sized vector segfaulted on a squared cost. What breaks: an NLP-sized var_vals on a problem with constraint sets and no hinge or absolute cost, which trajopt 0.35.0 read correctly; append zeros for the slack variables. A problem with no constraint sets and only squared costs has no slack variables, so nothing changes there. docs/breaking-changes.md has the migration (#149).

Changes

  • Eigen arguments accept lists and tuples — nanobind's Eigen caster imports an argument through DLPack or the buffer protocol only, so every Vector3d, VectorXd, Matrix3d or Matrix4d parameter raised TypeError for a list or tuple: Isometry3d.Identity().translate([1, 2, 3]), Isometry3d([x, y, z], q), env.setState(names, [..]), JointWaypoint(names, [..]), even the vector overload of Quaterniond.from_rpy([r, p, y]). One caster in tesseract_nb.h, seen by all 23 modules, wraps nanobind's: when nanobind refuses an argument and implicit conversion is allowed, it runs a sequence through numpy.asarray and asks nanobind again, so shape, dtype and layout checks are nanobind's. A wrong shape ([1, 2] for a Vector3d) still raises TypeError and nothing is padded or truncated; integers convert to float64; strings, None, complex numbers and ragged nesting raise TypeError, and an integer parameter (mesh faces) refuses floats. Parameters a function writes into (jacobianChangeBase, jacobianChangeRefPoint, twistChangeBase, twistChangeRefPoint, applyTolerances, harmonizeTowardZero, harmonizeTowardMedian) still take only a matching array. The stubs annotate an Eigen parameter as NDArray | Sequence[float] (Sequence[Sequence[float]] for a matrix), the in-place ones as NDArray alone; return annotations are unchanged.
  • Low-level SQP guide: why the online example rebuilds its problem, starting values, failure — the guide said the obstacle pose is "baked into" the collision constraints, untested. Measured: a problem built before env.setState moves the obstacle keeps a summed collision violation of 0.0 where a rebuilt one has 1.92, and perturbing the variables does not change that, so each evaluator's contact manager fixes the pose at construction, not its cache. Upstream's example rebuilds every tick too. New section: upstream's coefficients for the example and how to read SQPStatus and the remaining constraint violation (#12).
  • plan_freespace, plan_ompl and plan_cartesian take a composer — each call built a new TaskComposer and loaded the task composer plugins again, about 4.3 s on macOS arm64, so a 419-waypoint Descartes solve that takes 0.47 s through a held composer took about 5 s through plan_cartesian. A keyword-only composer= plans through the given composer and loads nothing; the caller owns its lifetime, so no module-level composer outlives interpreter teardown. Without one, the wrappers behave as before (#137).
  • The examples' run() and the benchmarks take a held composer — the planning examples' run() takes a keyword-only composer=; test_planning_benchmarks and test_parallel_scaling hold one composer per module and build the robot outside the timed call, so they time planning, not the plugin load. Re-measured without the load, a solve takes about 85 ms and more OMPL planners make it slower (8 planners: 1.6× in first-solution mode); the old table's "4 planners 1.58× faster" was plugin load, and run_scaling_benchmarks.sh no longer prints that conclusion regardless of the data (#137).
  • The planning guide's performance tips hold the composer — "Pre-Load Plugins" warmed a composer it never passed to plan_*, and "Profile Caching" cached profiles while every call still loaded the plugins; both now hold one composer and pass it (#137).
  • The getting-started pages run, and a test runs them — nothing executed them, and none of their plans could succeed: the quickstart and index targets were unreachable for the IRB2400, the README loaded package://tesseract_support/ (removed in tesseract 0.35) and its own box blocked the goal, concepts.md called methods removed since 0.33 and tcp_pose.position, which does not exist. Targets now come from upstream's freespace_example.cpp (the README drops the obstacle upstream does not have). tests/docs/test_getting_started_snippets.py runs every page's Python blocks in order, one process per page; a block is skipped only behind a visible <!-- snippet: skip --> marker (placeholder paths, the viewer, which binds port 8000), and every plan asserts success (#137).
  • Named TCPs: the composition contract documented and a parsed-SRDF roundtrip tested — group_tcps was readable since #133's read path landed, but nothing said how a named TCP composes or parsed one from an SRDF. A test parses an SRDF with three TCPs in one group and two in another (one name in both, with different transforms), reads each back exactly through SRDFModel.kinematics_information.group_tcps and Environment.getKinematicsInformation(), and resolves each with findTCPOffset. The API pages document the rule from the tesseract 0.35.0 sources: findTCPOffset's resolution order, the TCP pose T(tcp_frame) * offset, Cartesian targets relative to working_frame, IK on C * offset⁻¹, getCombined taking tcp_offset only with tcp_frame, the SRDF's scalar-first wxyz, and saveToFile writing TCPs at 6 significant digits (#133).
  • A Python exception keeps its type across extension modules — a Python ConstraintSet whose getValues raised IndexError reached the caller of TrustRegionSQPSolver.solve, init, TrajOptQPProblem.convexify or getExactCosts as RuntimeError on macOS, with the traceback as its message. Each extension links nanobind statically and so carries its own typeinfo for nanobind::python_error; macOS libc++ compares typeinfo by pointer, so _trajopt_sqp did not recognise the error thrown by _trajopt_ifopt's trampoline. Every module that defines a trampoline (tesseract_common, tesseract_collision, trajopt_ifopt, trajopt_sqp, tesseract_motion_planners_descartes) now registers a translator that restores the Python exception (#156).
  • build_macos_wheel.sh sets MACOSX_DEPLOYMENT_TARGET=14.0 — a local wheel build tagged the host's macOS (15.0) while every published macOS wheel is 14.0, which CI's wheels got only from their macos-14 runner. CI's wheel tags do not change (#156).
  • The missing Environment getters bound — getInitRevision, getTimestamp, getCurrentStateTimestamp, getJointLimits, getLinkCollisionEnabled, getLinkVisibility, getLinkTransforms (all three overloads), getCurrentFloatingJointValues (both) and getContactManagersPluginInfo. The timestamps return a naive local-time datetime.datetime. getLinkTransforms(names, values[, floating_joints]) returns the C++ out-param as a dict and validates its input like setState (ValueError). A name that is not in the scene graph raises KeyError from getJointLimits, getLinkCollisionEnabled, getLinkVisibility and getCurrentFloatingJointValues(names), instead of None or an arbitrary bool. getJointLimits returns a copy (#188).
  • The remaining plugin-info structs bound — ContactManagersPluginInfo, ProfilesPluginInfo and TaskComposerPluginInfo had no binding, so nothing that takes or returns one could be bound either; KinematicsPluginInfo lacked CONFIG_KEY. All four now carry a read-only CONFIG_KEY, and the three new classes compare by value and are unhashable (__hash__ = None), through a new shared bind_value_equality helper. AddContactManagersPluginInfoCommand is bound too, with value equality, and Environment.applyCommand accepts it; its getter returns a copy, so editing it cannot change a command already in an environment's history (#164).
  • Every ContactResult pair field raises on a wrong-length list — assigning type_id, link_names, shape_id, nearest_points or transform a list shorter than 2 kept the old value, and a longer list was cut to its first two items; neither raised. All pair fields are now bound directly on their native two-element arrays, so any other length raises TypeError, as cc_time and cc_type already did. The three pair fields that had no binding are added: subshape_id (default [-1, -1]), nearest_points_local and cc_transform, which a continuous contact test fills with the cast object's end pose (#172).
  • ContactResultMap readable by key, iterable and mutable — Python could only flatten a map. It now has at(key), getContainer() (a dict) and __iter__ over (key, ContactResultVector) pairs, plus addContactResult / setContactResult (single result or vector), shrinkToFit, filter(fn) and addInterpolatedCollisionResults. Every value handed out is a copy, and iteration walks a snapshot, so no Python reference into the map can dangle after an insert or shrinkToFit; only a filter callback gets the live vector, for the duration of the call. A missing key raises KeyError. Upstream only asserts two preconditions that the binding now enforces: a key with its link names out of order (which would be stored where no contact test looks) raises UnorderedLinkPairError, and an empty vector (whose last element upstream would read) raises EmptyContactResultsError; both subclass ValueError (#173).
  • Python can approve or reject contacts during a contact test — ContactResultValidator is bound as a subclassable class whose __call__(result) -> bool the managers call for each contact, and ContactRequest.is_valid takes one (None disables it). contactTest keeps releasing the GIL; the validator re-acquires it per call, so it works from worker threads, and an exception it raises propagates out of contactTest. The request keeps the validator object alive (#171).
  • ContinuousContactManager.setCollisionObjectsTransform takes start and end poses — the three cast (moving-object) overloads, (name, pose1, pose2), (names, poses1, poses2) and (transforms1, transforms2), were reachable only under the Python-only name setCollisionObjectsTransformCast. They are now overloads of the native name too, next to the three single-pose ones, as in C++. setCollisionObjectsTransformCast still works (#174).
  • ContactManagersPluginFactory binds its full API; managers keep the factory alive — Python can now list, add, remove and set the default discrete and continuous plugins, clear the search paths and libraries, create a manager from an explicit PluginInfo, and read (getConfig(), a YAML string) or save (saveConfig(path)) the config. Unknown names in remove… / setDefault… raise KeyError (upstream throws a generic runtime error), and saveConfig raises OSError when the file cannot be written (upstream ignores the failed write). Every create…ContactManager overload now ties the manager to the factory, since the manager's code lives in a plugin library the factory's loader owns (the gh-72 rule); before, dropping the factory first relied on the library staying mapped (#170).
  • tesseract_collision stubs name the geometry types — the collision extension never imported tesseract_geometry, so its stub spelled Geometry, Mesh and ConvexMesh as quoted C++ names ("tesseract::geometry::Geometry") in addCollisionObject, getCollisionObjectGeometries and makeConvexMesh, which type checkers cannot resolve. The extension now imports tesseract_geometry itself, and the stubs name the Python classes (#168).
  • createConvexHull bound; a failed hull raises ConvexHullError — Bullet's convex hull of a raw point set was reachable only through makeConvexMesh, which takes a Mesh. createConvexHull(input, shrink=-1, shrink_clamp=-1) returns the C++ out-params with the result as (n_faces, vertices, faces), where faces is the flat [count, i0, …] array. Upstream signals failure only by returning a negative face count, which happens when a positive shrink cannot be applied; the binding raises the new ConvexHullError (a RuntimeError) instead. An empty point set gives 0 faces, as upstream (#175).
  • V-HACD convex decomposition bound — ConvexDecompositionVHACD(params) splits a triangle mesh into a list of ConvexMesh hulls, configured by VHACDParameters (all ten fields, header defaults) and the FillMode enum under its native name; ConvexDecomposition is the abstract base, without a constructor. compute releases the GIL. Upstream reads the faces array without bounds checks, so the binding validates it first: a non-triangle face raises NonTriangleFaceError, a count running past the end or an out-of-range vertex index raises MalformedFacesError (both ValueError). The extension links tesseract's collision_vhacd component and does not compile a second V-HACD copy; a build-time #error and an nm test (Linux, macOS) guard that (#179).
  • Collision margin arithmetic on the margin types — CollisionMarginPairData and CollisionMarginData gain incrementMargins, scaleMargins, apply(pair_data, override_type) and the per-object getMaxCollisionMargin(obj); CollisionMarginPairData also gains getMaxCollisionMargin(). New constructors: CollisionMarginPairData({(a, b): margin, ...}), CollisionMarginData(default, pair_data) and CollisionMarginData(pair_data) (default margin 0). As upstream: the pair-data getters return None when no pair margin applies, while CollisionMarginData.getMaxCollisionMargin(obj) falls back to the default margin (#178).
  • Every Resource is a ResourceLocator; GeneralResourceLocator binds its API — Resource was bound without its C++ base, so locateResource could not be called on a BytesResource or SimpleLocatedResource, and a BytesResource could not take the parent locator that resolves its relative urls. Both BytesResource constructors now take parent=None, and their arguments are named. GeneralResourceLocator gains its two native constructors, GeneralResourceLocator(paths=[...], environment_variables=[...]) and GeneralResourceLocator(environment_variables=[...]), plus addPath and loadEnvironmentVariable. Both constructors take keyword arguments only: a positional list[str] could be directories or environment variable names, so it raises TypeError. locateResource is typed Resource | None (#166).
  • JointTrajectory and AddTrajectoryLinkCommand bound — JointTrajectory had no binding, so nothing that takes or returns one could be bound. It now has both constructors, states, description, uuid (as its canonical string; a malformed string raises ValueError), the list protocol (len, indexing with negative indices, assignment, iteration) and push_back, pop_back, front, back, at, clear, empty, reserve, capacity, shrink_to_fit, max_size. Element access returns a copy, so write an edited state back with traj[i] = js; an index out of range, and front / back / pop_back on an empty trajectory (undefined behaviour in C++), raise IndexError. It compares by value and is unhashable. AddTrajectoryLinkCommand(link_name, parent_link_name, trajectory, replace_allowed=False, method=AddTrajectoryLinkCommand.Method.PER_STATE_OBJECTS) is bound with its getters, value equality and the nested Method enum (no Method_* module constants), and Environment.applyCommand accepts it with the GIL released; getTrajectory() returns a copy (#167).
  • The rest of AllowedCollisionMatrix, makeOrderedLinkPair and getAllowedCollisions bound — AllowedCollisionMatrix(entries) builds an ACM from the dict that getAllAllowedCollisions() returns, ordering each key as upstream does. New: the single-link removeAllowedCollision(link_name), which drops every entry involving the link, reserveAllowedCollisionMatrix(size), and str(acm) (C++ operator<<, one line per entry). The free functions makeOrderedLinkPair(a, b) (a tuple, smaller name first) and getAllowedCollisions(link_names, acm_entries, remove_duplicates=True) are bound too (#180).
  • ManipulatorInfo(manipulator, working_frame, tcp_frame, tcp_offset=Isometry3d.Identity()), empty and getCombined bound — only the default constructor was bound, so Python built a ManipulatorInfo field by field. As upstream: empty() is True unless manipulator, working_frame and tcp_frame are all set, and getCombined(override) takes every non-empty field of override, with an overriding tcp_frame bringing its tcp_offset along. Setting tcp_offset to anything but a str or an Isometry3d now raises TypeError, in the constructor and on the field, instead of RuntimeError: std::bad_cast; reading it still returns a copy (#181).
  • KinematicLimits.jerk_limits and resize, isWithinLimits and enforceLimits bound — jerk_limits is a read-write (n, 2) array like the other limit fields, and resize(n) resizes all four. isWithinLimits(values, limits) checks the bounds with no tolerance (unlike satisfiesLimits). enforceLimits(values, limits) returns the clamped values as a new array and leaves its input unchanged; C++ clamps in place. Upstream checks neither function's sizes, and a release build reads past the shorter operand, so both raise the new LimitsSizeMismatchError (a ValueError) when len(values) differs from the number of limit rows (#182).
  • Frame and error math from tesseract/common/utils.h bound — twistChangeRefPoint, twistChangeBase, jacobianChangeBase, jacobianChangeRefPoint, calcRotationalError, calcTransformError, all four calcJacobianTransformErrorDiff overloads and applyTolerances, under their native names. Python can now re-base a calcJacobian result to another frame or reference point, and get the transform error diff that handles the angle-axis ±π discontinuity, without re-deriving either in numpy. The twist, jacobian and applyTolerances functions work in place, as in C++: they return None and write into the array passed in, and raise TypeError for an array they would have to convert first (float32, read-only, non-contiguous, or a C-order (6, n > 1) jacobian), so the result is never written into a copy and lost. The twist and jacobian shapes, (6,) and (6, n), are part of the signature. A tolerance pair that is neither both empty nor both of the right size raises the new ToleranceSizeMismatchError (a ValueError); upstream raises a bare RuntimeError for applyTolerances and does not check the error-diff overloads (#184).
  • The command history readable and replayable; CommandType bound — CommandType was unbound, so Command.getType() raised TypeError on every call. Environment.getCommandHistory(), applyCommands(commands) and init(commands) (the inverse of getCommandHistory: the list must start with an AddSceneGraphCommand) are bound, and CommandAppliedEvent.commands holds the history at the event, as a copy that stays valid after the callback. Command lists are polymorphic: every element comes back as its own class, AddTrajectoryLinkCommand included. SetActiveDiscreteContactManagerCommand and SetActiveContinuousContactManagerCommand are bound with value equality, and applyCommand accepts them. As upstream, only these commands are recorded in the history: Environment.setActiveDiscreteContactManager(name) and its continuous twin are not (#186).
  • Every Environment state, joint-value, link-name, group and contact-manager overload under its native name — getState gains its three C++ overloads: (joints_dict, floating_joints={}), (names, values, floating_joints={}) and (floating_joints). setState gains floating_joints on both forms and a (floating_joints) form. Also new: getCurrentJointValues(names), getActiveLinkNames(names), getStaticLinkNames(names), getJointGroup(name, joint_names), and getDiscreteContactManager(name) / getContinuousContactManager(name), which return a copy built from any registered plugin. getState now validates its input like setState (ValueError). Before, an unknown or fixed joint name was added to the returned state without complaint, and a names/values length mismatch read joint values past the end of the array. A floating_joints key that is not a floating joint raises ValueError from every getState and setState form, as it already did from getLinkTransforms. Upstream raises IndexError, and in setState it does so after storing the joint values. A name that is not an active joint raises KeyError from getCurrentJointValues(names), which raised IndexError before. An unknown joint raises ValueError from the link-name getters and getJointGroup(name, joint_names). A plugin name that is not registered raises KeyError from the by-name contact-manager getters, where C++ returns null. The by-name managers keep their Environment alive (the gh-72 rule). getStateByMap, getStateByNamesAndValues, setStateByNamesAndValues and getCurrentJointValuesByNames still work: each is bound to the same function as its native overload, so they gain the same validation and the floating_joints argument (#187).
  • EnvironmentContactAllowedValidator bound — the validator Environment installs on every contact manager it creates or clones came back from getContactAllowedValidator() as the bare ContactAllowedValidator base, and Python could not build one. It is now bound in tesseract_environment as a subclass of tesseract_common.ContactAllowedValidator, with its SceneGraph constructor only: the C++ default constructor exists for serialization and leaves a null scene graph that __call__ would dereference, so EnvironmentContactAllowedValidator() and EnvironmentContactAllowedValidator(None) raise TypeError. A pair is allowed exactly when the scene graph's ACM allows it; the validator holds the scene graph by shared pointer, so it follows later ACM changes and keeps the graph alive (#189).
  • checkTrajectoryState (both managers) and checkTrajectorySegment bound — the single-step collision checks from tesseract/environment/utils.h had no binding. C++ fills a ContactResultMap& out-param and returns void; Python gets the map as the return value, a new one per call, so nothing accumulates across calls as it does in C++. The GIL is released for the contact test. Upstream reads each active collision object's transform with state.at(link), so a state map missing one threw std::out_of_range after moving the links before it; the binding checks first and raises KeyError naming every missing link, leaving the manager untouched (#190).
  • cast_CommandAppliedEvent / cast_SceneStateChangedEvent check the event type — both were an unchecked C++ static_cast, so passing the other kind of event was undefined behaviour; in practice the call silently returned the event unchanged and the caller hit AttributeError later. They now raise the new EventTypeError (a TypeError) naming the event's Events value and the expected one. Neither cast is needed in an event callback: the callback already receives CommandAppliedEvent or SceneStateChangedEvent, never the bare Event. The parameter is named evt (#191).
  • Python can supply TCP offsets: Environment.addFindTCPOffsetCallback bound — findTCPOffset could resolve only an Isometry3d or an SRDF group TCP, because nothing could register a find-TCP callback from Python. addFindTCPOffsetCallback(fn) takes fn(manip_info) -> Isometry3d; findTCPOffset asks the callbacks in registration order for any other name. clone() copies them, and the environment keeps them alive. A callback takes the GIL itself, so clone() keeps running without the GIL and findTCPOffset works from task-composer worker threads. As upstream, a callback that raises, or returns anything but an Isometry3d, counts as "not located": the next one is asked, and when none returns a transform findTCPOffset raises RuntimeError ("Could not find tcp by name …") without the callback's own exception. getFindTCPOffsetCallbacks and getEventCallbacks stay unbound: they return C++ std::functions that Python can neither inspect nor call usefully (#183).
  • TaskComposer.get_available_pipelines() reads the factory; !include configs work — the method re-parsed the config file with PyYAML, which does not know the !include tag, so any config using it raised yaml.constructor.ConstructorError. It now takes the node names from the factory, which expanded !include when it loaded the config, and composer.py no longer imports yaml. TaskComposerPluginFactory.getTaskComposerNodePlugins() (dict[str, PluginInfo], a copy) and getAvailableTaskComposerNodePlugins() (the node factory class names the plugin libraries export, such as PipelineTaskFactory; not the configured names) are bound; the second loads the plugin libraries, so it pins them like createTaskComposerNode does (GH #48). A TaskComposer built without config_path now lists the factory's pipelines instead of []; config_path is informational (#185).
  • A None in a list of objects raises TypeError instead of crashing — nanobind rejects None for a direct object argument, but turns a None element of a list into a null pointer, which upstream dereferences later. CombinedContactAllowedValidator([None], …) segfaulted on its first call, addCollisionObject(name, mask, [box, None], poses) on either contact manager and the JointVelConstraint / JointAccelConstraint / JointJerkConstraint constructors segfaulted at once, and CompoundMesh([None, None]) stored null meshes. Each now raises TypeError naming the argument and index ("addCollisionObject: shapes[1] is None, expected a Geometry"), through one shared check that Environment.applyCommands / init(commands) already used (#201).
  • CalibrationInfo and SRDFModel.contact_managers_plugin_info bound — SRDFModel.calibration_info was bound but its type was not, so reading it raised TypeError and the stub quoted the C++ name; SRDFModel.contact_managers_plugin_info had no binding. tesseract_common.CalibrationInfo now has joints (dict[str, Isometry3d], a copy on read: assign the whole dict or use insert), insert(other) (overwrites by joint name), clear(), empty() and a read-only CONFIG_KEY ("calibration"). It compares by value with upstream's tolerance (each transform isApprox(1e-5)) and is unhashable. SRDFModel.contact_managers_plugin_info holds the ContactManagersPluginInfo the SRDF's <contact_managers_plugin_config> points at, and saveToFile writes both configs beside the SRDF (#217).
  • Core-module stubs name the scene-graph, state, environment and program types — tesseract_kinematics, tesseract_serialization, tesseract_srdf and tesseract_state_solver used types another extension defines without importing it, so their stubs spelled 33 annotations as quoted C++ names ("tesseract::scene_graph::SceneGraph", "tesseract::environment::Environment", …), which type checkers cannot resolve. Each extension now imports the defining modules itself: kinematics → tesseract_scene_graph, tesseract_state_solver; serialization → tesseract_command_language, tesseract_environment, tesseract_state_solver; srdf and state_solver → tesseract_scene_graph. tesseract_scene_graph.SceneState (the class bound in tesseract_state_solver) is now re-exported on first access instead of at import, since an eager re-export would make the state-solver → scene-graph import a cycle; from tesseract_robotics.tesseract_scene_graph import SceneState works as before (#218).
  • Every StateSolver state, Jacobian and link-transform overload under its native name, validated; insertSceneGraph bound — setState took only a full joint vector, the map and names-plus-values forms existed only as setStateByMap / setStateByNamesAndValues, no overload took floating_joint_values, and getLinkTransforms and MutableStateSolver.insertSceneGraph had no binding. setState now binds its four C++ overloads (vector, dict, names + values, each with floating_joint_values={}, and setState(floating_joint_values)), getState its five, getJacobian its three, and getLinkTransforms() / getLinkTransforms(names, values, floating_joint_values={}) (the C++ out-param returned as a dict). Every overload validates first: a joint name that is not an active joint, a floating name that is not a floating joint, names and values of different lengths, or a vector whose length is not the number of active joints raises ValueError; an unknown link in getJacobian raises KeyError. Before, OFKTStateSolver dereferenced a null node for an unknown joint name, so setStateByMap({"nope": 1.0}) crashed the interpreter (the GH #43 crash on the solver), and KDLStateSolver skipped it silently. setStateByMap and setStateByNamesAndValues still work: each is bound to the same function as its native overload, so they gain the validation and floating_joint_values. As upstream, KDLStateSolver ignores floating joint values and raises RuntimeError for setState(floating_joint_values) / getState(floating_joint_values). insertSceneGraph(scene_graph, joint, prefix="") returns upstream's bool (#219).
  • KinematicsPluginFactory binds its full plugin API; a group's last solver cannot be removed — Python could not list, add or remove a kinematics solver plugin, set a group's default solver, create a solver from an explicit PluginInfo, or read or save the config. KinematicsPluginFactory now binds add / get / remove / setDefault for FwdKinPlugin and InvKinPlugin (get…Plugins() returns a dict of group name → PluginInfoContainer), createFwdKin(solver_name, plugin_info, scene_graph, scene_state) / createInvKin(…) beside the group-name overloads, getConfig() (the YAML document as str) and saveConfig(file_path). As in ContactManagersPluginFactory (#170): setDefault… and remove… raise KeyError for an unknown group or solver, saveConfig raises OSError (FileNotFoundError for a missing directory) where upstream ignores a failed write, and every create… overload keeps the factory alive, since the solver runs code from a plugin library the factory loaded. Removing a group's last solver raises the new KinematicsPluginRemovalError (a RuntimeError) and leaves the factory unchanged: tesseract 0.35.0 erases the group and then reads, and for the default solver writes, through the erased iterator (kinematics_plugin_factory.cpp:150–154, reported upstream as tesseract#1381). getDefault…KinPlugin still raises RuntimeError for an unknown group (#211).
  • Every JointGroup.calcJacobian and KinematicGroup.calcInvKin overload under its native name; KinematicGroup constructor bound — calcJacobian took only (joint_angles, link_name), the link_point form existed only as calcJacobianWithPoint, and the two forms with a base_link_name (the frame the Jacobian is expressed in) were not bound; calcInvKin took a KinGroupIKInput or the opaque KinGroupIKInputs but not a list; KinematicGroup could only come from an Environment. calcJacobian now binds its four C++ overloads, (joint_angles, link_name), (…, link_name, link_point), (…, base_link_name, link_name) and (…, base_link_name, link_name, link_point), and every one raises KeyError for a link name the group does not have. Before, the two existing forms raised upstream's RuntimeError for an unknown link, and the new ones would have read an uninitialized transform in a release build (joint_group.cpp:197–198, :234–235). calcInvKin(list[KinGroupIKInput], seed) is bound. KinematicGroup(name, joint_names, inv_kin, scene_graph, scene_state) takes a clone of inv_kin, so the solver passed in stays usable, and keeps inv_kin (and so its factory) alive; upstream's checks raise RuntimeError. calcJacobianWithPoint and calcInvKinMultiple stay, bound to the same function as their native overloads (#212).
  • Kinematics utilities bound: numericalJacobian, calcManipulability, isNearSingularity, the harmonizers, isValid and checkKinematics — of tesseract/kinematics/utils.h only getRedundantSolutions was bound. Now: numericalJacobian (three overloads: a ForwardKinematics tip link, a JointGroup link, and a link relative to a base link with offsets; the 6 × n matrix is returned), calcManipulability with Manipulability / ManipulabilityEllipsoid (read-only fields), isNearSingularity(jacobian, threshold=0.01), harmonizeTowardZero / harmonizeTowardMedian (in place on a float64 array), isValid (six values) and checkKinematics(manip, tol=1e-3) from validate.h, which releases the GIL. Upstream reads or writes without checking in several of them, so the bindings check first: an unknown link raises KeyError and a wrong-length joint_values ValueError (numericalJacobian), a jacobian without 6 rows TypeError (calcManipulability), an empty matrix ValueError (isNearSingularity), and an index outside qs or position_limits IndexError with qs unchanged (the harmonizers). As upstream, a singular ellipsoid's measure and condition are sys.float_info.max, and checkKinematics returns True when IK finds no solution. solvePInv, dampedPInv and getRedundantSolutionsHelper stay unbound (#163) (#213).
  • Geometry.getUUID / setUUID bound — every geometry carries a UUID that == compares (Geometry::operator== is type and UUID, and each subclass's operator== starts from it), but Python could not read or set it, so Box(1, 1, 1) == Box(1, 1, 1) and geom == geom.clone() were False with no visible reason. getUUID() returns the canonical string; setUUID(str) sets it and raises ValueError for a malformed string, leaving the UUID unchanged, the spelling JointTrajectory.uuid uses. isIdentical still compares content (#206).
  • The full Mesh, SDFMesh and PolygonMesh constructors and the MeshTexture constructor bound — Python could build a mesh only from vertices and faces: Mesh and SDFMesh bound one (vertices, faces) constructor, PolygonMesh none, and MeshTexture had no constructor, so a mesh's resource, scale, normals, vertex colors, material and textures could be read back but never set. Each mesh class now binds both native constructors, (vertices, faces, resource=None, scale=…, normals=None, vertex_colors=None, mesh_material=None, mesh_textures=None) and the same with face_count after faces, with the C++ defaults (PolygonMesh also takes a trailing type=GeometryType.POLYGON_MESH); (vertices, faces) callers are unchanged. Upstream stores face_count without checking it; the binding counts the faces and raises the new MeshFaceCountError (a ValueError) when they disagree, before any object exists. MeshTexture(texture_image, uvs) takes a jpg or png Resource and the UVs; None for either raises TypeError. The API page's MeshMaterial and MeshTexture examples used attributes and a default constructor that never existed; they now use the bound constructors and getters (#207).
  • ConvexMesh.CreationMethod, getCreationMethod / setCreationMethod and the full ConvexMesh constructors bound; mesh classes compare with their own operator== — Python could not tell how a ConvexMesh was made, although the URDF parser records CONVERTED when tesseract:make_convex turned a mesh into its hull, and could build one only from vertices and faces. ConvexMesh.CreationMethod (DEFAULT, MESH, CONVERTED) is a nested enum, with no module constants; the getter and setter are bound, and ConvexMesh binds both native constructors through the #207 helper, so a wrong face_count raises MeshFaceCountError. PolygonMesh, Mesh, ConvexMesh and SDFMesh now bind their own operator== instead of inheriting Geometry's type-and-UUID comparison: two meshes with the same UUID compare unequal when their vertex count, face count or scale differ (for ConvexMesh also the creation method). SDFMesh keeps upstream's type-and-UUID comparison. ConvexMesh.clone() returns a DEFAULT mesh without normals, colors, material or textures, as upstream's does (convex_mesh.cpp:77) (#208).
  • createMeshFromBytes, createConvexMeshFromBytes and createSDFMeshFromBytes bound — Python could load a mesh from a path or a Resource, but not from bytes it already holds. The three instances of createMeshFromBytes<T> take (url, data: bytes, scale, triangulate, flatten, normals, vertex_colors, material_and_texture); the extension of url selects the format. The bytes are copied, so the meshes outlive data. triangulate defaults to True, as in the bound …FromPath / …FromResource siblings and the URDF parser, not false as in the header; the other flags keep upstream's False. The three …FromResource loaders, missing from tesseract_geometry.__all__, are added there with the new ones. createMeshFromAsset and extractMeshData take assimp types and stay unbound (#210).
  • The mesh loaders take normals, vertex_colors and material_and_texture — upstream's createMeshFromPath<T> / createMeshFromResource<T> take three flags to load a mesh's normals, vertex colors, materials and textures, but the six bound loaders took only (path_or_resource, scale, triangulate, flatten), so a textured .dae loaded from Python had none of them, although the same file parsed as a URDF visual did. All six now take the three flags after flatten, each False by default as in C++; with flatten and all three on, a loader returns what the URDF visual path returns. triangulate keeps its True default (#226).
  • Scene graph types print with upstream's operator<<; str(JointType.X) is upstream's name — JointDynamics, JointLimits, JointSafety, JointCalibration, JointMimic, ShortestPath and the JointType enum have an upstream operator<<, but none was bound, so print(joint.limits) showed a hand-written repr with two of six fields and print(path) the default object repr. Each now binds __str__ from it: str(JointLimits(-1, 1, 10, 2, 3, 4)) is "lower=-1 upper=1 effort=10 velocity=2 acceleration=3 jerk=4", and str(path) lists the links, joints and active joints. Changed: str(JointType.REVOLUTE) is now "Revolute" (upstream's spelling, "Unknown" for UNKNOWN), not "JointType.REVOLUTE"; .name and repr() are unchanged, so only code that formats a joint type with str() or an f-string sees the new text. The binding audit maps an operator<< on an enum to <Enum>.__str__ (#215).
  • Link.visible / collision_enabled, SceneGraph.setAllowedCollisionMatrix and getAdjacencyMap bound — Python could not hide a link or take it out of collision checking on the Link itself, although clone() copies both fields and == compares them; SceneGraph had no setAllowedCollisionMatrix and no getAdjacencyMap. Both fields are read/write. setAllowedCollisionMatrix(acm) keeps acm itself (later edits to it are edits to the graph's matrix, as with getAllowedCollisionMatrix); None raises TypeError and leaves the matrix in place, where upstream would store a null that the next ACM call dereferences. getAdjacencyMap(link_names) returns a dict mapping each listed link and every link below it, stopping at another listed link, to that listed link; an unknown name raises KeyError instead of upstream's RuntimeError. Link.getHash, SceneGraph.getVertex and getEdge stay unbound (#163) (#216).
  • tesseract_urdf.writeMeshToFile bound — tesseract/urdf/utils.h was not bound at all; its one function with a Python use is writeMeshToFile(mesh, filepath), which writes a mesh as the PLY file writeURDFFile references for every mesh it exports. It always writes ASCII PLY, whatever the extension; None for mesh raises TypeError (upstream dereferences it unchecked), and a path it cannot write raises RuntimeError. The urdf extension now imports tesseract_geometry, so its stub names PolygonMesh. The string helpers of the same header (toString, trailingSlash, noTrailingSlash, noLeadingSlash, makeURDFFilePath) stay unbound (#163). New API page tesseract_urdf (#220).
  • Kinematics examples for the new kinematics API — a new docs page, Kinematics Examples, and its examples, each a console script that returns its results for the test to assert on. kinematics_plugins_example builds IIWA kinematics from the plugin YAML with KinematicsPluginFactory(Path, locator), creates solvers by group name and by PluginInfo, switches the default inverse solver from KDL LMA to NR on upstream's runInvKinIIWATest problem (checking each solution with the solver's own stopping rule), adds, sets as default and removes a solver as upstream's PluginFactorAPIUnit does, round-trips saveConfig, and solves through a KinematicGroup built without an Environment. kinematics_analysis_example ports upstream's runJacobianTest: every native calcJacobian overload and the re-based ForwardKinematics Jacobian against all three numericalJacobian overloads, within a tolerance derived from the forward-difference step (δ = 1e-8: truncation plus rounding, 3.5e-6; measured 6.6e-8). It then shows calcManipulability and isNearSingularity at upstream's q and at the singular q = 0 (rank 5, three identical columns), the harmonizers, isValid, checkKinematics, KinematicLimits with isWithinLimits / enforceLimits, and the pose-error and tolerance-band functions on upstream's unit-test values. state_solver_example ports upstream's floating-joint test: OFKTStateSolver.replaceJoint makes the IIWA's joint_a1 FLOATING at x = 1.25 m, then the floating value moves to y = 1.5 m and z = 1.5 m. Every native setState, getState, getJacobian and getLinkTransforms overload runs with floating_joint_values, checked against a KDLStateSolver rebuilt from the scene graph after each change. insertSceneGraph attaches upstream's sub-graph through a FIXED joint, and again with a prefix through a FLOATING joint.
  • Convex hull faces list only their own polygon — upstream createConvexHull (convex_hull_utils.cpp:75–99, 0.35.0) reuses one face vector and never shrinks it, so every face after the first polygon also lists the 4th and later vertices of all earlier faces: a cube's hull came back with face sizes 4, 5, 6, 7, 8, 9 instead of six quads, and a V-HACD hull written to PLY and read back could lose a vertex. The vertices were right. createConvexHull, makeConvexMesh and ConvexDecompositionVHACD.compute now strip that prefix, keeping each ConvexMesh's vertices, face count, resource, scale and creation method. The repair checks the prefix before it strips it; faces without it (a fixed upstream) raise the new ConvexHullFaceRepairError (a RuntimeError) instead of losing vertices. Not repairable from the binding: hulls that upstream builds in C++, AddTrajectoryLinkCommand's convex hull methods and URDF tesseract:make_convex meshes, keep the corrupted faces until upstream is fixed (reported as tesseract#1382; removing the repair is tracked in #235) (#233).
  • ConstraintSet.getJacobian() before linkWithVariables raises ConstraintNotLinkedError instead of crashing — every built-in trajopt_ifopt constraint sizes its Jacobian from the linked variable set without a null check, so calling getJacobian() on a constraint not yet linked (directly or by adding it to a problem) segfaulted, for all ten bound constraint classes. The binding checks the link first and raises the new trajopt_ifopt.ConstraintNotLinkedError (a RuntimeError) naming the set. No other bound member reads the linked variables. A Python ConstraintSet subclass is unaffected: its own getJacobian replaces the bound one (#202).
  • Collision examples for the new collision API — a new docs page, Collision Examples, and its examples, each a console script that returns its results for the test to assert on. contact_managers_example ports upstream's box_box_example.cpp to all three discrete backends, built by name from ContactManagersPluginFactory(Path, locator). The backends agree on the colliding pair but not on penetration depth: FCL reports the analytic 1.3 m, Bullet 1.266 m on the box_2m.ply hull. At x = 1.6 the gap is one ulp above the 0.1 margin, so nothing is reported, as upstream's unit test expects. The example then runs upstream's pair-margin cases through the CollisionMarginData / CollisionMarginPairData constructors, incrementMargins, scaleMargins and apply, each checked by a contact test. It edits the factory as upstream's PluginFactorAPIUnit does, round-trips saveConfig, and builds upstream's TestGetAllowedCollisions ACM from a dict. A Python ContactResultValidator drops one contact pair, and the ContactResultMap is iterated and edited. A cast manager runs through the three sweep overloads of setCollisionObjectsTransform, reproducing upstream's cast-unit cc_transform and nearest_points_local points. On the IIWA environment it uses EnvironmentContactAllowedValidator, managers by name and the SetActive*ContactManagerCommand pair. trajectory_collision_example runs both checkTrajectoryState overloads and checkTrajectorySegment on upstream's boxbot boxes, 0.1 m in collision. FCL reports the analytic depth; Bullet reports 0.0577 m on these face-aligned boxes. It then rebuilds checkTrajectory's LVS_CONTINUOUS result on upstream's IIWA-and-sphere trajectory from checkTrajectorySegment and addInterpolatedCollisionResults. Each step equals checkTrajectory's, with upstream's 288 contacts. Equality needs one reused ContactResultMap, because clear() keeps its keys. Last, it builds a JointTrajectory with both constructors and applies AddTrajectoryLinkCommand with each Method: 14, 2, 7 and 1 collision shapes, as each method's semantics predict. convex_decomposition_example ports upstream's create_convex_decomposition tool, with its options, ranges and VHACDParameters defaults as argparse, plus a --fill_mode option. It splits the car-seat mesh into 64 V-HACD hulls, checks each against its own faces with an orient3d plane test (and, for the flat hulls V-HACD can return, an orient2d polygon test), and writes them as PLY files that read back to their written precision now that hull faces are repaired (#233). Each FillMode gives its own decomposition. A second createConvexHull of a hull's vertices is convex and keeps only those vertices, but it is not idempotent: Bullet merges a vertex within one step of its integer grid, and one RAYCAST_FILL hull loses a vertex 4 µm above a face.
  • CartLineConstraint.getJacobian() with fewer than 6 indices raises CartLineIndicesUnsupportedError instead of corrupting memory — trajopt 0.35.0 sizes the Jacobian by len(indices) rows but fills 6 in both the numeric and the analytic branch, so indices=[0, 1, 2] crashed most calls and could also return. CartLineConstraint now binds its own getJacobian: after the unlinked check (ConstraintNotLinkedError still comes first), it raises the new trajopt_ifopt.CartLineIndicesUnsupportedError (a ValueError) naming the set and the index count. Construction, getValues() and calcValues() still accept 1 to 6 indices. Direct Python calls only: a TrajOptQPProblem solve calls the C++ Jacobian and can still crash with fewer than 6 indices. A stopgap for #237; it goes when the pinned trajopt includes the fix (trajopt#618) (#238).
  • DiscreteContactManager.setCollisionObjectsTransform has one Mapping overload, and it releases the GIL — the binding had two map overloads, one taking the aligned TransformMap and one taking std::map. Both convert the same dict, so the stub listed setCollisionObjectsTransform(transforms: Mapping[str, Isometry3d]) twice, and nanobind, which takes the first overload that converts, never reached the second. Only that second overload was given a GIL release, so the release never applied. The unreachable std::map overload is removed, and the TransformMap overload, the one that runs, now releases the GIL during the transform update. ContinuousContactManager's single-pose map overload releases it too. A dict or any other Mapping converts as before (#241).
  • Environment examples for the new environment API — a new docs page, Environment Examples, and its examples, each a console script that returns its results for the test to assert on. environment_commands_example builds ABB IRB2400 environments with init(Path, Path, locator) and init(Path, locator) from GeneralResourceLocators made by each constructor, loadEnvironmentVariable and addPath, and checks getCommandHistory() against upstream's getInitCommands. An SRDF read with initString, carrying upstream's unit-test calibration as a fixture, adds one ChangeJointOrigin per calibrated joint; CalibrationInfo and SRDFModel.contact_managers_plugin_info are read back. On a URDF-only environment, AddContactManagersPluginInfoCommand makes the discrete manager available, applyCommands applies the calibration as one batch, and an event callback receives one CommandAppliedEvent per call. init(commands) replays a history: the histories, revisions and states are equal, the environments are not, because Environment == compares timestamps; a clone() is equal until a command is applied to it. The example spells out upstream's SceneState == comparison, which == now binds (#232). Last, BytesResource from bytes and from a list of byte values, whose parent locator resolves a relative url next to it. environment_state_example makes the IIWA's joint_a1 FLOATING with a ReplaceJointCommand and moves it through upstream's floating-joint origins, one setState overload per step; the three getState and three getLinkTransforms overloads compute states without setting them. It reads joint values by name, the links given joints move, a JointGroup over given joints, getJointLimits, the link visibility and collision flags, the revisions, and the two timestamps, which setState sets from one clock reading. findTCPOffset resolves an explicit pose, the SRDF's laser and welder group TCPs, and, after addFindTCPOffsetCallback, upstream's callback TCP; a link name or an unknown name raises RuntimeError. ManipulatorInfo's full constructor, empty and getCombined follow upstream's unit test. plugin_configuration_example builds contact_manager_plugins.yaml in Python as a Bullet part and an FCL part merged with insert; the result equals what the IIWA SRDF loads, and applied with AddContactManagersPluginInfoCommand to a URDF-only environment it creates each manager by name. ProfilesPluginInfo (upstream's unit-test values) and TaskComposerPluginInfo (the shipped config's libraries, executor and two pipelines) show the merge and empty / clear; no Python constructor consumes those two yet. TaskComposerPluginFactory.getTaskComposerNodePlugins() and getAvailableTaskComposerNodePlugins() show that every configured class is available and that get_available_pipelines() lists only configured names.
  • Mesh round-trip example for the new geometry API — a new docs page, Geometry Examples, and mesh_roundtrip_example, a console script that returns its results for the test to assert on. It loads upstream's box_2m.ply from bytes with createMeshFromBytes (the binding's triangulate=True default gives 12 triangles), createSDFMeshFromBytes and createConvexMeshFromBytes (triangulate=False keeps the 6 quads) (#210). tesseract_material_mesh.glb goes through each create*MeshFromPath and create*MeshFromResource loader with normals, vertex_colors and material_and_texture on, giving upstream's four meshes with normals and one textured quad (#226). That quad is rebuilt through both overloads of each full Mesh, SDFMesh, PolygonMesh and ConvexMesh constructor with a MeshTexture (#207). Mesh == compares UUIDs, so the rebuilt pair is unequal until setUUID (#206); ConvexMesh.CreationMethod changes ConvexMesh == (#208). writeMeshToFile (#220) writes PLY, and createMeshFromPath reads it back within its seven-decimal precision. The reader joins identical vertices, so a mesh with one vertex per normal comes back with one vertex per distinct position. Isometry3d.apply_points / apply_directions equal tf * p row by row, and PointCloud.Point (#209) fills a PointCloud for createOctree. The Basic Examples tip for loading meshes called a Mesh.fromFile that was never bound; it now uses createMeshFromPath. Wheels now ship the support mesh tesseract_material_mesh.glb: the support-data install rule copied files by an extension list that had no *.glb.
  • scene_graph_example builds a scene graph directly — a second, low-level part builds upstream's five-link test graph with SceneGraph, Link and Joint (one joint per JointType member, #214), and checks it against upstream's unit tests: adjacent links, children, getAdjacencyMap, isAcyclic / isTree through a disconnected link and a second parent, and the str() of JointType, the joint value types and a ShortestPath, each upstream's operator<< (#215). setAllowedCollisionMatrix replaces the matrix and the graph keeps the object; Link.visible / collision_enabled are the flags setLinkVisibility / setLinkCollisionEnabled set (#216). The example had no test; it now has one.
  • SceneState.floating_joints, getFloatingJointValues and == bound; the audit compares members of classes bound in another module — since #219 every StateSolver.setState / getState overload takes floating_joint_values, but the returned SceneState had no floating_joints, so Python could set floating joint values and not read them back. floating_joints is now a dict[str, Isometry3d] like link_transforms (a copy on read, assigned as a whole), getFloatingJointValues(joint_names) returns the named values and raises KeyError for a name that is not a floating joint (upstream throws std::out_of_range), and == / != are upstream's comparison: exact joint values, transforms within isApprox(1e-5); SceneState is unhashable. The binding audit missed the gap because SceneState is declared in a scene_graph header and bound in tesseract_state_solver: the class was accepted as bound in another module and its members dropped with it. They are now compared against the stub of the module that binds the class (#232).
  • SceneGraph.getJointChildrenNames takes a list of joint names — only the one-name overload was bound, so getJointChildrenNames(["joint_1", "joint_2"]) raised TypeError. The list overload returns the links below any of the joints, sorted and without duplicates, as upstream (graph.cpp:909–919); Environment calls it with the active joint names. A str still selects the one-name overload. The binding audit did not report the missing overload because it matches overloads by arity only, a documented limitation.
  • The binding audit matches overloads by parameter type — a C++ overload counted as bound when any Python overload of the same name took as many arguments, so SceneGraph.getJointChildrenNames(const std::vector<std::string>&) hid behind the bound getJointChildrenNames(str). scripts/audit_types.py now maps each C++ parameter type and each stub annotation to a key (std::vector<T> and Sequence[T], std::filesystem::path and str | os.PathLike, Eigen::VectorXd and NDArray[numpy.float64], a class and its subclasses, a container's typedef and its opaque bound name, …), and a Python overload covers a C++ one only where the keys agree. Overloads that differ only in const of this or &/&& of a parameter count as one. A type the mapping does not know (a template parameter, object) falls back to arity, and the report's new type fallbacks column counts those overloads. ContactResultMap.addInterpolatedCollisionResults is no longer misread as returning its in/out sub_segment_results. The 9 overloads the arity rule hid are bound or accepted: the plugin factories' YAML::Node constructors (rule yaml-node: the path and YAML-text overloads load the same config) and CompoundMesh's ConvexMesh/Mesh/SDFMesh list constructors (rule derived-element-container: upstream upcasts them into the PolygonMesh one, which Python already reaches with any subclass) are accepted; the rest are bound below (#247).
  • ChangeCollisionMarginsCommand(default_margin, pair_margin_data, override_type) — upstream's command that sets the default margin and the pair margins at once; override_type defaults to MODIFY, as upstream (#247).
  • CompoundMesh == compares its meshes — == fell through to Geometry.__eq__, which compares type and UUID only, so two compound meshes with different meshes and one UUID compared equal. == / != are now upstream's CompoundMesh::operator==, which also compares every mesh (#247).
  • OFKTStateSolver(root_name) — a solver holding the root link only, grown with addLink / moveLink (#247).
  • Isometry3d.apply_points and apply_directions: a batched apply — iso.apply_points(points) maps an (N, 3) float64 array in one call, where a Python loop over iso * p was the only way: about 126× slower than numpy at 2 000 points (osx-arm64, 0.35.0.8). Row i equals iso * points[i] bit for bit, because each row runs the same fixed-size Eigen product; a dynamic 3 × N product vectorises and fuses differently and came out up to 4e-16 apart. apply_directions applies the rotation only, as iso * v would with the translation zeroed. Both take N = 0 and strided views without a copy, convert Fortran-ordered and non-float64 input, and return a new array; a (3,) array raises TypeError (a single point stays iso * p). On osx-arm64 a call costs about 1 µs up to 24 points and 3.2 µs at 2 000, against 1.3–1.6 µs and 16.7 µs for numpy's p @ R.T + t. The Isometry3d docs said translation(), rotation() and matrix() were methods: they are read-only properties.
  • TrajOptQPProblem bound — the QP problem tesseract_planning's TrajOpt-Ifopt planner builds is now reachable from Python: construct it over the node variables (TrajOptQPProblem(createNodesVariables(...))) and add costs and constraints directly, with no IfoptProblem layer. Its exact merit weights each squared cost row by its coefficient, as its convex model does, and counts every cost term once, so for squared costs the trust-region ratio compares like with like; in trajopt 0.35.0, which every 0.35.0.x wheel bundles (0.35.0.8 and earlier bind only IfoptQPProblem), IfoptQPProblem does neither (trajopt#595). Two exceptions in trajopt 0.35.0, 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 but not its violation in the merit, and the model reads an ABSOLUTE or HINGE cost as 0 at every QP solution while its exact cost ignores its coefficient. One merit coefficient per constraint set. trajopt 0.35.0 defaults the move constructor in the header while the PIMPL type is incomplete, so nanobind binds a non-movable subclass that adds no state (#145).
  • ConstraintSet.getJacobian() returns every set's Jacobian — the collision constraints (analytic and numerical, discrete and continuous) assemble their Jacobian with coeffRef while a contact is active, which leaves it uncompressed, and nanobind's Eigen caster refused it: getJacobian() raised for exactly the rows a caller needs. The binding now returns a compressed copy.
  • JointPosConstraint's bounds constructor bound — one bound per joint (equality, one-sided or range) as an additive overload next to the target constructor; a range becomes two one-sided rows unless range_bound_handling=KEEP_AS_IS. In trajopt 0.35.0, which every 0.35.0.x wheel bundles, the range split indexes the caller's coeffs argument instead of the broadcast member coeffs_, so a length-0/1 coeffs is read past its end (fixed by trajopt#592, merged 2026-09-30, unreleased); the binding broadcasts first. Remove the broadcast only once a release containing #592 is pinned.
  • Python ConstraintSet subclasses report getNonZeros() == 0 — the Jacobian non-zero hint kept trajopt's unset default of −1, which Python could not change. TrajOptQPProblem reserves storage from the sum of its cost hints, so a Python cost set on its own raised ValueError: vector. 0 is a valid hint: storage grows as needed.
  • SQPCallback receives the problem itself, not a copy — nanobind's default automatic_reference policy copies lvalue-reference arguments, deep-copying the whole QP problem on every trial and aborting on a non-copyable problem (TrajOptQPProblem). The problem now arrives by reference (the same Python object that was passed to solve); results is still a per-trial copy.
  • Online SQP example: continuous mode works; collisions set up as in the C++ reference — build_optimization_problem(..., use_continuous_collision=True), which is also its default, raised RuntimeError: LVSDiscreteCollisionEvaluator, should be configured with LVS_DISCRETE, because the example never set the collision check type. online_planning_example.cpp sets DISCRETE for both modes, so its continuous mode throws the same way. The example now sets LVS_DISCRETE there. As the C++ reference does, it also sizes every collision set by the config's max_num_cnt (3; it passed 1), requests all contacts, and fixes the start of the first continuous segment. Its contract tests now run in both modes.
  • Docs follow the new problem — the replanning rates are re-measured on the TrajOptQPProblem example: about 70 Hz discrete and 20 Hz LVS continuous, on an Apple M1 Max. The 128 Hz "no collision" row is gone, because the example has no such mode. The trajopt_ifopt reference now lists, for each constraint class, its row bounds and the penalty types TrajOptQPProblem accepts it with (measured). It also gives the collision check type each evaluator requires; the constructors raise otherwise, and the online-planning page's continuous snippet used to miss it. The developer page on the constraint bindings, last current in 0.33, is rewritten.
  • TaskComposerPluginFactory accepts any ResourceLocator — the constructor and createTaskComposerPluginFactory raised TypeError: locator must be a GeneralResourceLocator for every other locator, including Python ResourceLocator subclasses, so a model locator could not be kept apart from the installation one. Both now bind the ResourceLocator base, as the C++ constructor does (#150).
  • TesseractViewer.close() shuts the server down — it called a _TesseractViewerAIOServer.close() that did not exist, printed the AttributeError and went on, leaving the port bound and the event loop open. The server now closes its websockets and connections and releases the port; close() closes the event loop, is idempotent, and raises instead of printing. aiohttp's shutdown only closes connections it has registered, and asyncio drops a connection accepted just before the listener closes (Server._attach asserts the server is open; the accepted socket leaks in CLOSE_WAIT), so the viewer owns its asyncio.Server and force-closes every connection it accepted. start_serve_background() now returns once the port is listening, not before, and bind errors reach the caller; serve_forever() called a misspelled method and failed at once (#150).
  • applyCommand releases the GIL for the two commands that carry geometry — adding a link whose collision geometry is a mesh spends seconds inside Environment::applyCommand building collision shapes, all of it holding the GIL, so every other Python thread stalled on what is pure C++ work. applyCommand(AddLinkCommand) and applyCommand(AddSceneGraphCommand) now drop the GIL around the C++ call, matching the solve() / contactTest guards, as do getDiscreteContactManager / getContinuousContactManager — applyCommand only updates a contact manager that is already cached, so with a cold cache the shape build lands in the first getter call instead. EventCallbackFn now re-acquires the GIL, since the environment fires event callbacks from inside that released region (and from C++ threads such as the ROS 2 monitor). Known ceiling: a registered Python event callback plus a concurrent Python thread calling a non-releasing Environment method can deadlock, because upstream fires event callbacks under the environment's unique lock.
  • Environment.clone() releases the GIL — cloning a large environment held the GIL for the whole native deep copy, so a worker cloning off the UI thread still froze it. Audited against tesseract 0.35.0: Environment::clone holds the environment's shared lock (serialised against setState / applyCommand), and the copy touches no Python state — event callbacks are not cloned, find_tcp_cb has no Python binding, and Python-owned resources are only shared_ptr-copied. A private _GilProbe, attached as a find-TCP callback that clone() copies, records whether each copy ran with the GIL, so the test checks the release directly instead of timing another thread; a timed version failed on loaded CI runners (#134).
  • create_descartes_default_profiles documents the real sample_max default — the docstring said +pi; the library default is pi/2, pi minus the default sampling resolution, so -pi and +pi are not both sampled. A caller who changes sample_resolution sets sample_max with it.
  • Typing stubs match the bindings; CI fails on drift — the committed .pyi stubs had not been regenerated since the 0.35 namespace change: they still spelled tesseract_common::…, lacked ContinuousContactManager.getCollisionObjectGeometries / getCollisionObjectGeometriesTransforms (#157), and had no stub at all for tesseract_serialization. All stubs are regenerated. pixi run stubs regenerates them and pixi run stubs-check fails on any difference. Every CI build job runs the check, so a binding change without a regenerated stub no longer gets in. Stubgen output depends on the nanobind version, so nanobind is now pinned to exactly 2.12.0 (#159).
  • Four bindings that failed from Python now work — each showed up in the stubs as a quoted std:: C++ type, which also made the stubs differ between platforms:
  • TaskComposerFuture.waitFor raised TypeError on return; it now returns the new FutureStatus enum (ready / timeout / deferred).
  • Resource.getResourceContentStream raised TypeError too; it now returns the contents as an io.BytesIO.
  • Tesseract maps that use Eigen::aligned_allocator (e.g. TransformMap) had no Python conversion, because nanobind's unordered_map caster matches default-allocator maps only. They now convert to and from dict.
  • KinematicsPluginFactory found FilesystemPath only if tesseract_common had been imported first. Kinematics now imports it itself.

A test rejects any std:: type left in a stub (#159). - checkTrajectory, satisfiesLimits and the continuous contact fields bound (#158): - tesseract_environment.checkTrajectory binds all four C++ overloads: a discrete or continuous manager, each with either a StateSolver plus joint names or a JointGroup. The C++ contacts out-parameter is returned instead: the call returns (ContactTrajectoryResults, list[ContactResultMap]). The GIL is released while the trajectory is checked. - ContactTrajectoryResults, ContactTrajectoryStepResults and ContactTrajectorySubstepResults are new in tesseract_collision. Their table and summary methods return str. - ContactResult.cc_time and cc_type (two entries each) are bound. Assigning a list of any other length raises TypeError. - tesseract_common.satisfiesLimits has a scalar-tolerance overload, with the C++ defaults (max_diff=1e-6, max_rel_diff = double epsilon), and a per-joint-tolerance overload.

0.35.0.8 - 2026-09-18 — TrajOptIfopt tuning + Cartesian planning fixes

  • Cartesian planning works with the default pipeline — plan_cartesian() and TaskComposer.plan_cartesian() now select the registered, double-precision DescartesDPipeline; the previous DescartesPipeline name did not exist in the generated configuration. Pipeline names and Descartes setup-cost guidance are corrected in the API and user guides (74c00ef9).
  • Planning failures identify the failing task — PlanningResult.message now reports the failed node and its specific diagnostic, such as a TrajOpt iteration limit or a failed contact check, instead of hiding it behind ErrorTask: Error (Abort Triggered) (74c00ef9).
  • Basic Cartesian examples match the C++ scene and tool orientation — both the high-level and low-level examples now build the reference obstacle from point-cloud data as an octree, replacing the smaller solid box, and use the reference TrajOpt profiles. The Eigen quaternion is correctly reordered to scalar-last [0, 1, 0, 0], restoring the intended downward tool orientation (74c00ef9).
  • macOS OpenMP troubleshooting — documented the duplicate libomp runtime problem when a bundled wheel is installed alongside conda NumPy, including diagnosis and installation guidance for multithreaded Descartes crashes; the packaging limitation remains unresolved (74c00ef9).
  • TrajOptIfopt collision configs and SQP parameters bound — TrajOptIfoptDefaultCompositeProfile exposed only smoothing and TrajOptIfoptSolverProfile only OSQP settings, so from Python the IFOPT planner ran on C++ collision and SQP-loop defaults with no way to reach them. collision_cost_config / collision_constraint_config (trajopt_common::TrajOptCollisionConfig) and opt_params (trajopt_sqp::SQPParameters) are now bound. opt_params sits on the base solver profile — create() assigns it straight onto the TrustRegionSQPSolver, making it the only route to max_iterations (50), max_qp_solver_failures (3), the trust-region ratios, and merit-coeff inflation; the sco back-end has always exposed these via TrajOptOSQPSolverProfile, so an IFOPT migration silently lost them. The collision configs carry max_num_cnt, which only trajopt_ifopt honours (the constraint size must be fixed): it caps the collision constraint at that many rows per timestep, one per worst link pair, so QP size stops tracking contact count (eb92e15).
  • tesseract_motion_planners_trajopt_ifopt re-exports the types its profiles need — TrajOptCollisionConfig, SQPParameters, and CollisionEvaluatorType are re-exported from the planner module, matching tesseract_motion_planners_trajopt, so configuring a composite or solver profile no longer means importing from trajopt_ifopt / trajopt_sqp / tesseract_collision alongside it. The module's __all__ also regains TrajOptIfoptMoveProfile, TrajOptIfoptDefaultMoveProfile, and ProfileDictionary_addTrajOptIfoptMoveProfile, which the stub declared but the package never re-exported.
  • CartPosConstraint's per-axis coefficients + bounds constructor bound — only the equality-form ctor was wired, so from Python the constraint was an equality on all six axes at unit weight, with no way to free an axis or give one an asymmetric bound. The second C++ ctor (coeffs, std::vector<Bounds>) is now an additive overload, so existing callers are unaffected; toBounds(lower, upper) was already exposed to build the band. See Low-Level SQP API → Per-axis Cartesian constraints in the user guide for what the coefficients and RangeBoundHandling do.
  • TrajOpt factory collision margins — create_trajopt_default_profiles() and create_freespace_pipeline_profiles() carried C++ car_seat_example.cpp's collision buffers and coefficients but not its margins, so default_margin stayed None and the collision cost held nothing off obstacles: over OMPL seeds 1-100 the freespace example passed a median 0.17 mm from its sphere and failed the pipeline's contact check on 3. The constraint now gets C++'s 0 m margin and the cost its 5 mm: closest pass 4.98 mm, no failures (#103, #135).
  • Low-level TrajOptIfopt example parameterizes time with ISP — TOTG failed 14 of 200 OMPL seeds on TrajOptIfopt's trajectories ("Negative path velocity"); ISP, which tesseract's own TrajOptIfoptTask runs, failed none (#103, #135).
  • OMPL example tests hold for every seed — the per-test seed reaches OMPL only on Linux and macOS (conda-forge's Windows ompl.lib is static), so it hid the two failures above rather than preventing them. The seeds that exposed them replay as tests, and a seed-free test holds the freespace path at least half the cost margin off its obstacle (#103, #135).
  • Margin vs buffer, docs and puzzle-piece ports — the migration guide mapped 0.2x's safety_margin onto collision_margin_buffer, which trajopt_common documents as "not used when calculating the error"; it maps onto contact_manager_config.default_margin. Corrected across the migration, planning, TrajOpt API and concepts pages, and the three puzzle-piece examples now set C++'s 25 mm cost margin (#135).

0.35.0.7 - 2026-07-24 — multi-brand emitter subsystem + coordinated external axes

  • Brand-independent multi-dialect emitter — the ABB-only RAPID emitter (shipped in 0.35.0.1) is generalised into a typed intermediate representation and lowering pass feeding per-dialect backends over a shared ProgramBackend ABC, so a new controller language is a backend, not a fork. Five dialects emit from one tesseract CompositeInstruction — ABB RAPID, KUKA KRL, Fanuc LS (TP ASCII), Yaskawa Motoman JBI (INFORM), and Universal Robots URScript — all exported from tesseract_robotics.emitters with a mermaid architecture page. RAPID is reseated onto the core IR and decomposed into profile / targets / emit / backend modules; its public surface (emit_rapid, RapidProfile, rapid_writer) is preserved through re-exports, so existing callers are unaffected (#128).
  • External-axis (eax) emission for coordinated motion — RAPID robtarget / jointtarget now carry real external-axis values (linear track in mm, rotary positioner in deg) instead of the 9E9 sentinel, split off the joint vector by joint name so a positioner ordered ahead of the arm can't bleed into the arm axes; external_axes=None stays byte-identical to prior output. Emitted programs also gain an ASCII-safe generated header and explicit UTF-8 writes so they survive Windows' locale codec (#127, #128).
  • createInvKin / createFwdKin teardown SIGSEGV fixed — the returned solver holds raw pointers into the scene graph, scene state, and plugin factory, so nb::keep_alive binds those to the solver's lifetime (the factory owns the dylib whose vtables the solver dispatches through). Coupled ROP/REP external-axis solvers no longer double-free at interpreter shutdown — the same destruction-order class fixed for plugin getters in 0.35.0.5 (gh-72), now closed for the kinematics factory functions (#126).

0.35.0.6 - 2026-07-04 — runtime kinematics commands + SQP GIL release

  • AddKinematicsInformationCommand + KinematicsPluginInfo bound — inverse/forward-kinematics solver plugins and named kinematic groups can be added to an environment at runtime from Python via an environment command, instead of only through the SRDF at load time (#125, e51946f).
  • GIL released in TrustRegionSQPSolver.solve (plus FK and setCollisionObjectsTransform) — mirrors the collision and motion-planner call_guard<gil_scoped_release> guards so long SQP optimisations run off the main thread without blocking other Python work (#123, 0f12535).

0.35.0.5 - 2026-06-20 — Descartes in the high-level API + collision GIL release + teardown & wheel fixes

  • contactTest releases the GIL — parallel collision checking across cores — DiscreteContactManager.contactTest and ContinuousContactManager.contactTest now bind with nb::call_guard<nb::gil_scoped_release>(), mirroring the motion planners' solve() guards. Without it a Python worker thread sweeping a trajectory for collisions only time-shared the GIL with the main thread (concurrency, not parallelism — the sweep didn't get faster and contended with the UI); with it, sweeping on per-thread cloned managers scales near-linearly — measured 1.95× on 2 threads (10-core machine) against a 0.99× pure-Python control that rules out a measurement artifact. Enables off-main-thread, sharded collision scans of a whole program (#122).
  • Plugin-backed objects keep their Environment alive — fixes the gh-72 SIGSEGV lottery — getKinematicGroup / getJointGroup / getDiscreteContactManager / getContinuousContactManager now bind with nb::keep_alive<0, 1>. These objects are plugin-created (OPW/KDL/UR kinematics, bullet/fcl collision factories) and their vtables live in dylibs owned by the Environment's plugin loader; when Python destroyed the Environment first (interpreter-teardown dict order, pytest GC), the loader dlclosed the plugin dylibs and the survivors' destructors virtual-called into unmapped pages. Whether a given script crashed was pure destruction-order luck — the reported "calcJacobian segfaults under pytest but works in plain Python" was this lottery, not the jacobian path (lldb forensics: dangling vptr pinned inside unloaded libtesseract_kinematics_opw.dylib) (#72).
  • Linux wheels: libstdc++ no longer bundled — fixes dual-copy heap corruption — the ldd-closure bundling swept the conda env's gcc-14 libstdc++.so.6 into every wheel; numpy/scipy load the system copy, the loader unifies STB_GNU_UNIQUE locale statics across the two, and std::regex inside PropertyTree::rebuildAutoValidators freed a stack address during Environment.init (SIGSEGV/double free or corruption — detection-dependent UB, so the pytest suite corrupted silently). The gcc runtime (libstdc++, libgcc_s) is now excluded from bundling, the GH #35 RTLD_GLOBAL preload is removed (its "two copies coexist safely" premise was falsified), and a test-wheel CI guard asserts ships-no-copy / one-copy-mapped / numpy-first env.init clean. Wheels now require a gcc-13+ era system or conda libstdc++ (ubuntu 24.04+, debian 13+); older distros fail loudly at import. Full forensics: docs/developer/linux-wheels.md (#119).
  • Basic Descartes example — descartes_raster_c_api_example.py, low-level port of upstream motion_planners/examples/raster_example.cpp: a 3-pass raster program solved by DescartesMotionPlannerD directly (no TaskComposer) with TrajOpt refinement chained on the result; CompositeInstruction gains the full C++ constructor (profile, manipulator_info, order) so UNORDERED transition composites are constructible from Python (#87).
  • Descartes woven into the high-level planning API — new create_descartes_pipeline_profiles() for the standalone DescartesF/D(NPC)Pipeline family, and create_descartes_default_profiles() grows the 0.35 knob surface: tool-axis rotation sampling for axis-symmetric processes (sample_axis/sample_resolution/sample_min/sample_max → target_pose_fixed=False), ik_solver override, use_redundant_joint_solutions, and move_profile= injection of custom (Python-subclassable, #83) DescartesMoveProfileD instances — mutually exclusive with the knobs, fails loud (#85).
  • Lowlevel planning example: TrajOpt profiles reach the solver — tesseract_planning_lowlevel_c_api_example.py added its TrajOpt plan/composite profiles to the OMPL dictionary while handing the solver an empty trajopt_profiles; TrajOpt silently planned on fallback defaults (#115).
  • get_task_composer_config_path() resolves like the runtime — returns the @PLUGIN_PATH@-patched config that _configure_environment() publishes via TESSERACT_TASK_COMPOSER_CONFIG_FILE (env var → bundled data → conda share) instead of unconditionally returning the bundled-only path, which doesn't exist in dev envs and was unpatched in wheels; raises the new TaskComposerConfigNotFoundError when nothing resolves; the hand-rolled fallback chains in the task composer tests, TaskComposer.from_config, and scripts/generate_pipeline_dotgraphs.py now delegate to it (#110).
  • ruff enforces the py39 floor — FA102 added to lint select so PEP 604 unions without from __future__ import annotations fail the hooks; with target-version = "py39" (rejects 3.10+ syntax) this turns 3.9 compatibility from convention into a gate (#92).
  • Symmetric shared_ptr<Environment> serialization writers — the environment write path now mirrors the reader, fixing the polymorphic cast that broke environment_from_* deserialisation round-trips (#121, bcac229).

0.35.0.4 - 2026-06-08 — conda-forge C++ deps + OSQP tunability + Linux libstdc++ preload + CI hardening

  • OSQP solver settings exposed — OSQPEigenSolver gains seven tunable setters (setPolish, setWarmStart, setAdaptiveRho, setMaxIteration, setAbsoluteTolerance, setRelativeTolerance, setVerbosity) and TrajOptIfoptOSQPSolverProfile gains setAdaptiveRhoInterval, enabling iteration-based (deterministic) rho adaptation for reproducible SQP solves; fixes dangling-pointer segfault in getJacobian() via rv_policy::reference_internal on getVar/getNode/getNodes (0f40048, c984044, c083f5e, #103).
  • OMPL RNG determinism — RNG_setSeed/RNG_getSeed bound in tesseract_motion_planners_ompl; conftest.py auto-reseeds per test for xdist-safe reproducibility; single-planner + optimize=False recipe documented as the only guaranteeable determinism contract (parallel-plan best-of-N races even with seeded samplers); Windows determinism test dropped — conda-forge ompl.lib embeds a separate RNGSeedGenerator copy per DLL so seeding the extension can't reach the planner (a0f49c6, 5f4776e, 31c7541, #103).
  • conda-forge C++ deps — C++ stack migrated from colcon workspace build to conda-forge prebuilts; pixi install && pixi run build replaces pixi run build-cpp / colcon; pixi.lock v7 with eigen-abi-tagged orocos-kdl _2 ending heap corruption in all KDL paths; locked:true + pinned pixi-version in CI so stale-lock drift fails loud; build-dir cache keyed on pixi.lock hash; Python 3.9 feature env added for Rhino 8 (b6a14c4, 3c80a25, 6210a99, 0455e34).
  • Linux libstdc++ preload — bundled gcc-14 libstdc++ loaded RTLD_GLOBAL as the first act of __init__ wins the SONAME race before numpy/scipy can pull in the system copy on Ubuntu 22.04 and older; gated on CONDA_PREFIX absence to avoid double-loading inside pixi/conda envs (#35, ee83dc8, 7715422).
  • Environment.setState input validation — unknown or non-active (fixed/mimic) joint names now raise ValueError with a clear message instead of silently dereferencing unmapped entries and crashing with SIGSEGV; all three entry points covered (dict, names+values, setStateByNamesAndValues) (#43, ca620d3).
  • SceneGraph type registration in URDF module — _tesseract_urdf now imports _tesseract_scene_graph at module load so the return type of parseURDFFile is always registered; was masked by full-suite imports, exposed by the new minimal-import ABI canary (f944293).
  • CI hardening — Windows test-wheel now runs the full suite (kinematics + examples, benchmarks-only exclusion); macOS nanobind build directory cached to halve warm builds; ABI canary (install wheel into pixi env → kinematics smoke-test) on Linux and macOS build jobs catches dep-ABI rot in ~30 s instead of as scattered segfaults in test-wheel; duplicate CI env block merged (e60f272, b29c37a, 04a812f, 6210a99, 607b4b9).
  • Dotgraph polish — TaskComposer pipeline diagrams woven into docs; clean_dot_labels strips dump-debug fields for legibility; contract test + type annotations (4aa5efb, 57537da, 1bfb4c3).

0.35.0.3 - 2026-06-05 — complete wheel matrix for the 0.35.0.2 content

Reissue of 0.35.0.2 with the full wheel set actually published. In 0.35.0.2 a dirty CI build tree made setuptools_scm stamp a PEP 440 local segment (+d<date>) onto some wheels; PyPI rejects those with HTTP 400, and the non-idempotent publish step aborted mid-upload — so macOS (all) and Linux x86_64 (cp39/cp311) wheels never landed. 0.35.0.2 is yanked. Identical bindings to 0.35.0.2 — no API changes (49f1aa9).

  • Release pipeline hardened — setuptools_scm local_scheme = "no-local-version" so a dirty tag build can't produce a PyPI-rejected local version; skip-existing: true on all three publish workflows so re-runs fill gaps instead of 400-aborting; macOS fail-fast: false so one flaky build can't cancel sibling wheels; plus a 3× retry on the macOS upload-artifact step working around GitHub's long-standing arm64-runner CreateArtifact timeout (actions/upload-artifact#569, still open) that otherwise skips the entire macOS publish ~80% of the time (49f1aa9, 804d5b8).
  • cp312-abi3 support verified on all platforms — macOS and Windows test-wheel now install the cp312-abi3 wheel on Python 3.13/3.14 (mirroring Linux; wheel != python), so the published 3.9–3.14 support matrix is verified in CI, not just asserted (14a4cea).

0.35.0.2 - 2026-06-04 — aarch64 / Jetson Linux wheels + collision manager bindings

Extends Linux wheel coverage to aarch64 (including NVIDIA Jetson), completes the contact-manager binding surface, and hardens the new ROS RPY decomposition against gimbal-lock edge cases (#97, 669f2da, f30014b).

  • aarch64 / NVIDIA Jetson Linux wheels — new GitHub Actions matrix entry plus Jetson build patches, built for Python 3.12+ on aarch64; Linux wheel CI standardised on pixi run build-cpp / build-wheel (#52, dc3f1df, 605dc4d, af7bc4e, #91, ba0510f, c897251, @Joelkang).
  • Collision manager bindings completed — remaining discrete/continuous contact-manager surface plus Descartes MoveProfile contact-manager config (#94, 9bfd6b1, 7a3b0e3, @Joelkang).

0.35.0.1 - 2026-05-29 — tesseract 0.35.0 upgrade + Eigen geometry suite + RAPID emitter

Major dependency bump to tesseract 0.35.0, a complete Eigen geometry binding surface with scalar-last quaternions as the project canon, a Pose that now is an Isometry3d, and a new ABB RAPID code emitter. (First wheel release of the 0.35 series; the project's setuptools_scm scheme requires four-component tags, so it is 0.35.0.1, not 0.35.0.)

  • Complete Eigen geometry binding suite — fleshed-out Vector3d / Vector4d / Matrix3d / Quaterniond / AngleAxisd / Isometry3d surface, with scalar-last [qx, qy, qz, qw] Quaterniond.from_xyzw and Quaterniond.from_rpy / to_rpy ZYX ROS RPY interop; float64 default precision centralised via Eigen NumTraits (#76, 492ab7a, 544f7dd, 1d6f5c7, ef26fc9).
  • ABB RAPID code emitter for tesseract_command_language (tesseract_robotics.emitters.rapid) (#75, 37f21b2).
  • Octree geometry bindings (plus octomap::OcTree, OctreeSubType, PointCloud, createOctree) and the remaining tesseract_geometry utilities (isIdentical, extractVertices, toTriangleMesh) (#67, 4575aa9, @Joelkang).
  • Python-subclassable Descartes evaluators/sampler/profile for tesseract_motion_planners_descartes — new bindings for DescartesStateD, DescartesStateSampleD, DescartesEdgeEvaluatorD, DescartesWaypointSamplerD, DescartesStateEvaluatorD, and a now-subclassable DescartesMoveProfileD let users override createWaypointSampler / createEdgeEvaluator / createStateEvaluator from Python (#83, 620b228, @Joelkang).
  • Upgraded to tesseract 0.35.0 — deps bumped (rosinstall → repos), MIGRATION.md sed applied, namespace aliases added, resource URIs and dev paths updated for the 0.35 layout, wheel data dir tesseract_support → tesseract/support, kinematics_core_factories → kinematics_factories, descartes tp::MoveInstructionPoly → tcl::MoveInstructionPoly (#86, 6710ccf, 422c5ea, 4e08a36).
  • Pose now subclasses Isometry3d (tesseract_robotics.planning) — rotation math via Eigen, numpy-property facade dropped, factories collapsed to one name accepting both scalar-positional and arraylike call styles, Transform alias removed in favour of the single canonical Pose, stored-data accessors exposed as properties (#76, a202a45, caa3b19).
  • Scalar-last quaternions declared project canon — [qx, qy, qz, qw]; Quaterniond(w, x, y, z) literals migrated to Quaterniond.from_xyzw (0bd408d, f7b2fea).
  • README gains an examples section and a refreshed platform-support matrix (d7bde99, 8eed899).
  • KinematicsInformation.group_tcps exposed to Python (tesseract_srdf) (#79, 52fdfba).
  • TaskComposerNodeInfoContainer returns TaskComposerNodeInfo (#81, 106d162, @Joelkang).
  • getSeed() exposed on CartesianWaypoint (#80, 0c57074, @Joelkang).
  • Viewer light color (#73, 3b4c545, @johnwason).

0.34.1.7 - 2026-05-09 — command_language I/O instructions + TaskComposer introspection

Python API surface expansion: tesseract_command_language gains its remaining I/O and pacing instruction types (Wait, Timer, SetAnalog, SetDigital, SetTool) — closing the gap that previously forced raster and pick-and-place pipelines through C++ or YAML — alongside TaskComposer DAG visualisation via GraphViz DOT output and TaskComposerDataStorage introspection helpers. Welcomes @seonixx (Simon White) as a new contributor.

  • Remaining tesseract_command_language instruction types bound: WaitInstruction, TimerInstruction, SetAnalogInstruction, SetDigitalInstruction, SetToolInstruction, with their WaitInstructionType / TimerInstructionType enums and InstructionPoly_as_* cast helpers. Closes the I/O-and-pacing gap that previously forced raster/pick-and-place pipelines to be authored in C++ or YAML. New tests/tesseract_command_language/test_command_language.py covers construction, casting, and InstructionPoly round-tripping (#70, a4c26df, @Joelkang).
  • TaskComposer DAG visualisation — getDotgraph() / saveDotgraph() overloads on TaskComposerNode, with an optional TaskComposerNodeInfoContainer argument to highlight the executed path on a previously-run DAG. Emits GraphViz DOT for debugging otherwise-opaque planning pipelines (#63, e7c3a92, @Joelkang).
  • TaskComposerDataStorage AnyPoly cast/wrap helpers — AnyPoly_wrap_TaskComposerDataStorage and AnyPoly_as_TaskComposerDataStorage, so DAG storage state can be inspected after each node from Python instead of only via C++ (#66, b30ed46, @Joelkang).
  • TaskComposerDataStorage.getAllData() no-arg overload binding returning the full data map (#66, 990705f, @seonixx).
  • DiscreteContactCheckTask exposes contact_results through task_infos data_storage, with unit and integration test coverage (#66, 4fe07bf, 06178f0, @seonixx).
  • SimplePlanner and time-parameterization profiles now reachable via the public tesseract_motion_planners_simple and tesseract_time_parameterization package paths instead of only the underscore-prefixed C++ extension modules. C++ binding-side registrations retired in favour of __init__.py re-exports; test_constant_tcp.py consolidated into test_time_parameterization.py (#62, d73e227, 30c8e9f, @Joelkang).
  • InstructionPoly_as_CompositeInstruction re-exported from tesseract_command_language package init — was bound but not surfaced (#70, 110e1e0, @Joelkang).

0.34.1.6 - 2026-04-27 — PyPI README refresh

Same wheels as 0.34.1.5; recut so the published wheel's README metadata includes the Windows build badge alongside Linux and macOS on PyPI's project page.

  • README badge row includes a Windows build badge (6656815).

0.34.1.5 - 2026-04-27 — Windows x64 wheels debut + Python profile bindings

First complete cross-platform PyPI release: Linux, macOS, and Windows wheels for Python 3.9–3.12. Closes the long-running #40 Windows wheels initiative (#58). Also ships a batch of profile-binding contributions from @Joelkang that had been queued in Unreleased.

  • Windows x64 wheels published to PyPI via the new wheels-windows.yml workflow. Mirrors wheels-{linux,macos}.yml; sources C++ deps from conda-forge via pixi; bundles plugin DLLs via delvewheel repair --analyze-existing (f9c122d, 3e3a2e4).
  • __init__.py prepends the package dir and tesseract_robotics_nanobind.libs/ to PATH on Windows — required because boost::dll's plain LoadLibrary ignores os.add_dll_directory (006ce38).
  • Cereal polymorphic types re-registered in the tesseract_serialization binding TU — MSVC instantiates the cereal registry per-DLL, so upstream's registrations don't reach the consumer .pyd without re-registration (af88515).
  • New developer doc docs/developer/windows-wheels.md capturing four Windows wheel packaging gotchas surfaced during the gh-40 diagnosis (fe5ad75).
  • CompositeInstruction.push_back now accepts a CompositeInstruction in addition to a plain Instruction, enabling nested composites (e.g. raster programs) to be built from Python the same way they are in C++ (#51, 6d17314, @Joelkang).
  • SimplePlanner profiles configurable from Python — SimplePlannerCompositeProfile plus seven concrete move profiles (FixedSize, FixedSizeAssign, FixedSizeAssignNoIK, LVS, LVSNoIK, LVSAssign, LVSAssignNoIK), with ProfileDictionary_addSimplePlannerMoveProfile / …CompositeProfile helpers for registration (#54, e2bf837, @Joelkang).
  • task_composer planning-node profiles exposed through a new tesseract_task_composer_planning module: ContactCheckProfile, FixStateBoundsProfile, FixStateCollisionProfile, KinematicLimitsCheckProfile, MinLengthProfile, ProfileSwitchProfile, and UpsampleTrajectoryProfile. Heavy TrajOpt-typed members on FixStateCollisionProfile (OSQP settings, SQP params, coeff data) are intentionally skipped — configure those via YAML or C++ (#56, 62cd2cd, @Joelkang).
  • ConstantTCPSpeedParameterization (KDL-based, constant TCP-speed time parameterization) bound alongside its ConstantTCPSpeedCompositeProfile (#55, f4f981d, @Joelkang).
  • vcpkg-based wheels-windows.yml variant (workflow_dispatch-only, never green) replaced by the conda-forge / pixi pipeline of the same name (3e3a2e4).

0.34.1.4 - 2026-04-27 [YANKED] — partial cross-platform release

Yanked. Linux and macOS wheels published successfully; Windows wheels built and tested but the publish job hit a PyPI Trusted Publisher configuration mismatch and didn't upload. Superseded by 0.34.1.5.

0.34.1.2 - 2026-04-23 — macOS arm64 stability

Follow-up to the macOS arm64 debut in 0.34.1.1, resolving a garbage-collection crash that surfaced in fresh virtual environments during motion planning.

  • Dedupe versioned dylibs in the wheel — the actual root cause of the macOS GC crash (#48, 7f53185).
  • Pin plugin dylibs on macOS to prevent a garbage-collection crash during motion planning (#48, 590343f).
  • Restore the CI macOS test scope that had been narrowed as a workaround for #48 (#49, 07f8f9c).

0.34.1.1 - 2026-04-23 — macOS arm64 wheels debut

First PyPI-published macOS arm64 wheels, shipping via a dedicated wheels-macos.yml workflow parallel to the existing Linux x86_64 pipeline. Also introduces a Python trampoline for ConstraintSet, Python 3.9 compatibility for example modules, and installable example scripts.

  • macOS arm64 wheels published to PyPI via a split, per-platform workflow (d80e689).
  • Python trampoline for ifopt.ConstraintSet so users can subclass it from Python for custom SQP constraints (361c60e).
  • Examples installable as console scripts (e.g. tesseract_basic_cartesian_example) via [project.scripts] (2c62952, @marip8).
  • pixi run build-wheel task for local wheel builds (e26df2a).
  • Non-benchmark tests fail loud instead of being silently skipped (a1d7607).
  • Python 3.9 compatibility for example modules via from __future__ import annotations (87ce68e).