Changelog¶
Unreleased¶
Breaking changes¶
CollisionCoeffData.setPairCollisionCoeff/getPairCollisionCoeffremoved — copies ofsetCollisionCoeff/getCollisionCoeffunder names C++ does not have. Call the C++ names (#298).CollisionCoeffData,TrajOptCollisionConfigandSQPParameterscompare by value and are unhashable —==compared identity;hash(),setmembers anddictkeys now raiseTypeError(#261).- The collision constraints reject a
Varoutside a variable set —DiscreteCollisionConstraint,DiscreteCollisionNumericalConstraint,ContinuousCollisionConstraintand the new collision classes raiseVarNotInVariableSetError(aValueError) at construction when theVaris not in aNodesVariables. trajopt reads the set's hash through theVar's parent pointers unchecked, so such a constraint dereferenced null on its firstupdate(). AVarfromNode.addVarqualifies once itsNodeis handed toNodesVariables(#297). OSQPEigenSolver's update methods check sizes —updateGradient,updateLowerBound,updateUpperBoundandupdateBoundsraiseQPSizeError(aValueError) on a vector whose length is not theinit()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(),setmembers anddictkeys raiseTypeError.MoveInstructionequality includes both UUIDs (#261;docs/breaking-changes.mdhas the migration). - Time-parameterization profiles compare by value and are unhashable —
TOTGCompositeProfile,ISPCompositeProfile,ISPMoveProfileandConstantTCPSpeedCompositeProfilebind__eq__/__ne__from the C++operator==(identity before) and set__hash__ = None, as #169 did for collision, common and environment:hash(),setmembers anddictkeys raiseTypeError(#261). -
The seven simple-planner move profiles compare by value and are unhashable —
SimplePlannerFixedSize{,Assign,AssignNoIK}MoveProfileandSimplePlannerLVS{,NoIK,Assign,AssignNoIK}MoveProfilebind the C++operator==;hash(),setmembers anddictkeys raiseTypeError(#261). -
OMPLSolverConfig.getNumPlanners()removed;plannersis readable —solver_config.plannersbinds 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, socfg.planners[0].range = 0.5edits the config. To change which planners it holds, assign a new list, or useaddPlanner/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,BasicTrustRegionSQPParametersand the seventesseract_task_composer_planningprofiles bind the C++operator==;hash(),setmembers anddictkeys raiseTypeError.OMPLSolverConfigcomparesplannersby count only, as upstream (#261). create_ompl_planner_configuratorsrejects a keyword that fits none of the requested planners — it accepts everyOMPLPlannerTypename and sets each keyword on the configurators that have that field; a keyword no requested configurator has raisesValueErrorinstead of being ignored (#301).- TrajOpt-Ifopt and Descartes profiles and waypoint configs compare by value and are unhashable —
TrajOptIfoptCartesianWaypointConfig,TrajOptIfoptJointWaypointConfig,TrajOptIfoptDefaultMoveProfile,TrajOptIfoptDefaultCompositeProfile,TrajOptIfoptOSQPSolverProfile,DescartesDefaultMoveProfileDandDescartesLadderGraphSolverProfileDbind__eq__/__ne__from the C++operator==and set__hash__ = None;hash(),setmembers anddictkeys raiseTypeError(#261).
Changes¶
- Motion-planner core: the rest of
utils.h, request reasons, robot configuration —toToolpath(three overloads),formatProgramandcontactCheckProgram(discrete and continuous; returns(ContactTrajectoryResults, list[ContactResultMap])with the GIL released) (#303);MotionPlanner.checkRequestReason(request) -> strandMotionPlanner.assignSolution(#332);RobotConfig,getRobotConfig,getJointTurns,isWithinJointLimits(#333;isValidState<double>does not compile upstream and stays unbound). The module imports the modules that defineEnvironment,CompositeInstructionandProfileDictionary, so its stub names them instead of quoting C++ names (#300). - Simple planner —
SimpleMotionPlannerderives fromMotionPlanner(#299);interpolate(poses and joints),interpolate_waypointandgetInterpolatedInstructions(#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,LazyPRMstarConfiguratorandSPARSConfiguratorjoin the three already bound, one perOMPLPlannerType, with every public field.createstays unbound: it takes OMPL types (#301). OSQPSettingsandTrajOptOSQPSolverProfile.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 fieldslinsys_solverandcg_precondare not bound (#306).TrajOptDefaultCompositeProfile.computeLongestValidSegmentLength— the static helper that turns a longest-valid-segment fraction and length into the collision-check step (#307).FixStateCollisionProfilebinds its TrajOpt correction fields —trajopt_joint_constraint_config,trajopt_joint_cost_config,collision_constraint_coeff,collision_cost_coeff,opt_paramsandosqp_settings;tesseract_task_composer_planningnow importstesseract_motion_planners_trajoptandtrajopt_ifoptat load (#326).OMPLMotionPlannerandTrajOptMotionPlannerderive fromMotionPlanner—isinstance(planner, MotionPlanner)holds, andMotionPlanner.solve(planner, request)dispatches to the planner (#299).- Profile hooks
OMPLMoveProfile.createSolverConfigandTrajOptSolverProfile.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 keyProfileDictionaryuses, 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_yamlon the OMPL, TrajOpt and task-composer planning profiles —OMPLRealVectorMoveProfile,TrajOptDefaultMoveProfile,TrajOptDefaultCompositeProfile,TrajOptOSQPSolverProfileand the seventesseract_task_composer_planningprofiles 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: {...}resetstypetoDISCRETE;solver_config: {...}starts with no planners). Malformed YAML raisesValueError, a value of the wrong type the profile'sRuntimeError(#331).- TrajOpt-Ifopt and Descartes planners are
MotionPlanners —TrajOptIfoptMotionPlannerandDescartesMotionPlannerDderive fromtesseract_motion_planners.MotionPlanner;getName/solve/terminate/clear/cloneare the base's (itssolvereleases the GIL) (#299). - The Descartes stub names
Environment— the binding importstesseract_environment, soDescartesMoveProfileD.create*no longer quote"tesseract::environment::Environment"(#300). - Python SQP callbacks reach the TrajOpt-Ifopt planner —
TrajOptIfoptSolverProfile.callbacks(copied on access;Nonerejected) andcreateOptimizationCallbacks(). On trajopt 0.35.0 a callback returningFalsedoes not stop the solve (upstreamstepSQPSolverignores the stopped status) (#308). - The TrajOpt-Ifopt term factories —
createJointPositionConstraint,createCollisionConstraints,createJointVelocityConstraint,createJointAccelerationConstraint,createJointJerkConstraintfromtrajopt_ifopt_utils.h; an empty orNone-holdingvarsraises (#309). DescartesStateSampleD(state)andstr()on Descartes waypoint samplers — the one-argument constructor (cost 0) and__str__from upstream'sprint(#310).- Descartes tool-pose samplers —
sampleFixed,sampleToolAxis,sampleToolXAxis,sampleToolYAxis,sampleToolZAxis, returningtesseract_common.VectorIsometry3d(#311). - TrajOpt-Ifopt profile hooks —
TrajOptIfoptMoveProfile.create,TrajOptIfoptCompositeProfile.createandTrajOptIfoptSolverProfile.create, with the structs they return:TrajOptIfoptTermInfosand the move-onlyTrajOptIfoptWaypointInfo(itsnoderead-only and keeping the info alive) (#339). createKey()on the TrajOpt-Ifopt and Descartes profile bases —TrajOptIfopt{Move,Composite,Solver}Profile.createKey()andDescartes{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_yamlandTrajOptIfoptOSQPSolverProfile.from_yaml(config)run the C++ YAML constructor; malformed YAML raisesValueError, a value of the wrong type the constructor'sRuntimeError(#331). - Python-authored simple-planner move profiles — subclass
SimplePlannerMoveProfileand overridegenerate; its instruction and manipulator-info arguments arrive as copies, so an override may keep them. Ageneratethat returns an empty list or a null instruction raisesSimplePlannerEmptyGenerateError/SimplePlannerNullInstructionError(bothValueError; upstream would read past the end or plan the null as a success).super().generate(...)from such an override raisesSimplePlannerPureVirtualCallError(aNotImplementedError; the base is pure virtual). An exceptiongenerateraises propagates fromSimpleMotionPlanner.solvewith its type when the plan fails (the last one raised wins; upstream turns it into a failed response, and theMotionPlannerbasesolveand the task composer still do). A profile withoutgenerateraisesSimplePlannerPureVirtualCallErrorfromsolve, ageneratereturning anything but a list ofMoveInstructionPolyraisesSimplePlannerProfileReturnTypeError(aTypeErrornaming the offending element's type), and one whose last instruction differs frombase_instructionin move type, waypoint type, profile or path profile raisesSimplePlannerGenerateMismatchError(aValueError; 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 twelveinterpolate{Joint,Cart}{Joint,Cart}Waypointoverloads and bothgetClosestJointSolutionoverloads (the pair one returns a tuple).JointGroupInstructionInfo.getWorkingFrameis declared upstream but never defined and stays unbound (#340). DescartesMoveProfileD.createKinematicGroup(manip_info, env)— the helper a C++ Descartes override calls; the returnedKinematicGroupkeepsenvalive, whose kinematics plugins hold its IK solver code (#340).- Python-authored TrajOpt-Ifopt move and composite profiles — subclass
TrajOptIfoptMoveProfileorTrajOptIfoptCompositeProfileand overridecreate; a composite profile can add PythonConstraintSets to the terms it returns, and those sets are released safely on a task-composer worker thread. An exceptioncreateraises propagates fromsolvewith its type.super().create(...)from such an override, or a subclass withoutcreate, raisesTrajOptIfoptPureVirtualCallError(aNotImplementedError). A move profile'screatemust 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 raisesTrajOptIfoptProfileReturnTypeError. A compositecreatethat changes the planner's (read-only) nodes raisesTrajOptIfoptNodeLayoutChangedError: added variables would change the problem's layout, and changed values would be silently overwritten by the planner's seed.TrajOptIfoptSolverProfile.createstays non-overridable: it returns the concreteTrustRegionSQPSolver(#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 raisesProfileOverrideNotDispatchedError(aTypeError, exported by each planner module), naming the abstract base to subclass instead (#340). - Docs build with Zensical — the site builds with
zensical build --strictinstead of MkDocs + Material for MkDocs (maintenance ends 5 November 2026; MkDocs 2.0 drops plugins).mkdocs.ymlis read unchanged and renders the same 54 pages; strict mode now also fails on broken anchors. Zensical andmkdocstrings>=1need Python ≥3.11, so the docs tooling moves out of the default pixi feature into adocsenv (Python 3.12, no C++ stack):pixi run docs/pixi run docs-buildrun there (#252). - Command-language bindings complete — instruction UUIDs as canonical strings (
getUUID/setUUID/getParentUUID/setParentUUID/regenerateUUIDon the nine instruction types, #262);copy.copy/copy.deepcopyon the waypoints, instructions and Polys (#263);WaypointPoly,MoveInstructionPolyandInstructionPolybuilt from the types they wrap, the fullMoveInstruction(path profile,manipulator_info) and five-argumentStateWaypointconstructors (#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);CompositeInstructioninsert,__delitem__,pop_back,front/back/atand capacity members (#269), traversal (flatten,getFirst/Last(Move)Instruction, counts,moveFilter, #270) andgetUserData/setUserData(#271);ProfileDictionary.addProfilefor several names,getProfileEntry,getAllProfileEntries(#272); theutils.hfunctions (toJointTrajectory, joint-position helpers,clampToJointLimits,makeTimeMonotonicallyIncreasing,toDelimitedFile, #274;validateSeedStructureis not bound: declared inutils.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_kinematicsandtesseract_environment, so its signatures and stub nameJointGroup,KinematicGroupandEnvironmentinstead of quoted C++ names (#285). - The planner's collision constraints —
DiscreteCollisionConstraintD,ContinuousCollisionConstraintD(one row per contact) andContinuousCollisionNumericalConstraintare bound;trajopt_ifoptandtrajopt_sqpdrop__all__, so the star import re-exports every bound name,toStringincluded (#291, #284). - Collision evaluator queries —
calcCollisions/calcCollisionDatareturn aCollisionCacheData;getGradient,calcGradientDataandgetJointGroupare bound on the two evaluator bases, with thetrajopt_commonresult structs (LinkGradientResults,GradientResults,LinkMaxError,GradientResultsSet,CollisionCacheData) (#292). Bounds += s/Bounds -= sshift both limits and reclassify the bounds type; the name keeps theBounds(#286).Component.getValues/getBounds,Differentiable.getJacobianandDifferentiable.Modeare bound on the bases (#287).- Constraint accessors —
setBoundson every constraint that declares it (raisesBoundsSizeErrorunless one bound per row),CartLineConstraint.getLine/getInfo,InverseKinematicsConstraint.setTargetPose/getKinematicInfo(#288). calcJacobianBlock(joint_vals)onCartPosConstraint,CartLineConstraintandDiscreteCollisionNumericalConstraint: the Jacobian at given joint values, sized likegetJacobian(); raisesJointValuesSizeErroron a wrong length.InverseKinematicsConstraint.calcJacobianBlockstays unbound: trajopt 0.35.0 declares it but does not define it (#289).CartLineInfo(...)/InverseKinematicsInfo(...)field constructors, with the C++ defaults as overloads. Thelist[Bounds]constructors ofJointVel/JointAccel/JointJerkConstraintstay 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)andcalcForwardNumJac(f, x, epsilon)check an analytic Jacobian, a PythonConstraintSet's included (#295).toTrajArray(vars)/toJointTrajectory(vars); the online SQP example reads its warm start withtoTrajArray(#296).NodesVariables(name, nodes)and theVarconstructors — Python builds variable sets with several vars or per-node bounds; theNodes hand over ownership.Var.getParent/Node.getParent(#297).QPProblemconvex model andsetVariables—getHessian,getGradient,getConstraintMatrix,getBoundsLower,getBoundsUpper,getConstraintMeritCoeff, andsetVariables(x), which raisesVariablesSizeErrorunlesslen(x) == getNumNLPVars()(#282).QPSolverupdate interface —updateHessianMatrix,updateGradient,updateLowerBound,updateUpperBound,updateBounds,updateLinearConstraintsMatrixandsetWarmStart(problem)on the base;OSQPEigenSolver.setWarmStarttakes abool(the OSQP option) or aQPProblem(#283).
Fixed¶
- A
VarorNoderead out of a variable set keeps the set alive —Node.getVar/addVar,NodesVariables.getNode/getNodesandVar.getParenttie the result to its source, and aNodehanded toNodesVariableskeeps the set alive;var.getParent()after the set was dropped read freed memory (#297). NodesVariables.getNode/getNodesreturn usableNodes while the caller still holds the handed-over ones — they returned the caller's relinquished object, on which every method raisedTypeError. ANodegiven twice, or one already in a set, raisesNodeOwnershipError(aValueError) and leaves everyNodewith the caller (#297).- Linking a constraint to a variable set that does not hold its
Vars raisesVarNotInVariableSetError— fromlinkWithVariablesand fromTrajOptQPProblem.addConstraintSet/addCostSet; a standaloneVaror one from another set put the Jacobian's columns out of range (#297). Node.getParentno longer reads a freed variable set — it returned a raw pointer to theNode's set, which a planner frees after its solve while a Python composite profile'screatemay still hold theNode. It now returns a set Python built (NodesVariables(...),createNodesVariables) only while that set is alive and holds theNode, and keeps it alive; otherwise it raisesNodeParentUnavailableError(aRuntimeError). Linking a constraint checks that the set holds itsVars instead of comparing addresses, so aVarwhose set was freed cannot pass for a member of a new set at the reused address. An unlinked collision constraint built on aVarkept from a planner's set raises the same error fromupdate()instead of reading the freed set; one on a set built from Python updates as before. A Python subclass ofNodesVariablesis its owngetParent()result, andNodesVariables(name, "...")with astrfornodesraises a clearTypeError(#297).calcForwardNumJacraisesNumericalJacobianSizeError(aValueError) whenfreturns a different length at a perturbedx; trajopt read past the shorter vector (#295).CartPosConstraint.calcJacobianBlocktakes its joint count from itsVar— it dividedgetNonZeros()bygetRows(), and accepted any length with every axis weighted zero (#289).- A stored
SQPCallbackproblem can't outlive the call — a problem with no Python object (oneTrajOptIfoptMotionPlannerbuilds in C++) reaches a Python callback as aQPProblemView; once the call returns, every method on it raisesQPProblemExpiredError(aRuntimeError). A callback that kept the problem used to read freed memory aftersolve()returned. A problem passed tosolve()from Python still arrives as that object (#308). InstructionsTrajectory(program)keepsprogramalive — 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 aRuntimeError(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 newStaleTrajectoryError(aRuntimeError): build a new trajectory.setDatastill writes into the program. Cost per call: +0.3 µs on an 11-move program, +4.7 µs on 1000 moves.getPosition,getVelocity,getAcceleration,getTimeFromStartandsetDataraiseIndexErrorforioutside[0, size())(upstream indexes unchecked); negativeiis rejected, not wrapped (#281).TimeParameterization.computenamesEnvironmentin its signature — the stub said"tesseract::environment::Environment"; the module now importstesseract_environmentfirst (#277).- Ruckig jerk-limited smoothing —
RuckigTrajectorySmoothingandRuckigTrajectorySmoothingCompositeProfile(duration_extension_fraction,max_duration_extension_factor,override_limits,velocity_limits,acceleration_limits,jerk_limits) are bound; the module linkstesseract::time_parameterization_ruckig(#278). rescaleTimings(program, scalings)— rescales each sub-composite's timing in place; ascalingslist that is notlen(program)long raises the newScalingsLengthError(aValueError; upstream only asserts) (#279).ISPCompositeProfile.minimum_time_deltais bound (#280).createKey()on the time-parameterization profiles — a staticX.createKey()returns theProfileDictionarykeyX().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_infoandTaskComposer.planguessed the TCP as the last entry ofgetActiveLinkNames()(an order that holds for a serial chain, not a tree),MotionProgramandget_manipulator_infodefaultedworking_frameto the literal"base_link", andget_manipulator_infocached the guess and handed every caller the same mutableManipulatorInfo.Robot.fk/Robot.ikguessedtip_linkthe same way.tcp_frameandworking_frameare now required inMotionProgramandget_manipulator_info,tip_linkinfkandik; a missing one raisesTypeErrorat the call. Both take a keyword-onlytcp_offset(str | Isometry3d, default identity) that goes toManipulatorInfo.tcp_offsetunchanged, so a named SRDF TCP reaches the planner, which resolves it withEnvironment.findTCPOffset.get_manipulator_infobuilds a new object per call;to_composite_instructionloses itstcp_frameoverride, 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::pathispathlib.Path;Environment.init(str, …)always means content —tesseract_common.FilesystemPathwas astrsubclass, soEnvironment.initcould not tell a path from URDF content:init(str, str, locator)treated both strings as paths (the opposite of the C++std::stringoverload),init(str, locator)guessed from a leading<, and the Python-onlyinitFromUrdf/initFromUrdfSrdfexisted to work around the guess.std::filesystem::pathnow converts with nanobind's caster (str | os.PathLikein,pathlib.Pathout), andEnvironment.initbinds the four native overloads: astris URDF/SRDF content, apathlib.Path(anyos.PathLike) is a file. Where a path overload sits beside a content overload (Environment.init×2, theContactManagersPluginFactoryandKinematicsPluginFactoryconfig constructors), the path parameter acceptsos.PathLikeonly, so astrnever reaches it and a mixedinit(content, Path, locator)raisesTypeError. Removed:FilesystemPath,_FilesystemPath,TransformMap(a plaindictconverts) andEnvironment.initFromUrdf/initFromUrdfSrdf. A path left asstrmakesinitreturnFalse; 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_environmentandtesseract_command_languageno longer re-export task-composer functions —tesseract_environment/__init__.pyimportedAnyPoly_wrap_EnvironmentConstfromtesseract_task_composerinsidetry: … except ImportError: pass, andtesseract_command_language/__init__.pydid the same forAnyPoly_wrap_CompositeInstruction,AnyPoly_wrap_ProfileDictionaryandAnyPoly_as_CompositeInstruction, behind a_HAS_TASK_COMPOSERflag. A broken task-composer extension (a missing dylib, say) made the names vanish silently, andfrom tesseract_robotics.tesseract_environment import *then raisedAttributeErrorfor 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_COMPOSERare removed; import the functions fromtesseract_task_composer, where they are defined. Importing either package no longer importstesseract_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_environmentandtesseract_commonexported 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/_REPLACEand the module-levelCONSOLE_BRIDGE_LOG_DEBUG/_INFO/_WARN/_ERROR/_NONE. C++ has no such constants (theCONSOLE_BRIDGE_LOG_*names areLogLevelenumerators, not module names), and each enum is bound as a class. All 14 constants and their__all__entries are gone: importing one raisesImportError, reading one as a module attribute raisesAttributeError. 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.setDefaultCollisionMarginDataand.setPairCollisionMarginData,tesseract_common.CollisionMarginOverrideType, andCollisionMarginData.getPairCollisionMargin/.setPairCollisionMargin. Using one raisesAttributeError(ImportErrorforfrom tesseract_robotics.tesseract_common import CollisionMarginOverrideType). TheContactManagerConfigexample in the collision API page used aconfig.margin_datamember that never existed; it now setsdefault_margin,pair_margin_dataandpair_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/createTaskComposerExecutorraiseTaskComposerPluginErrorinstead of returningNone— 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 asNonealthough the stub promised aTaskComposerNode/TaskComposerExecutor. Callers got anAttributeErrorlater, far from the cause, andTaskComposer.warmup/get_available_pipelinescounted an unknown or unbuildable name as loaded, since theirexceptnever fired. Both methods now raise the newTaskComposerPluginError(aRuntimeError): 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.warmupandget_available_pipelinescatch it, andTaskComposer.planreturns its message in the failedPlanningResult(#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,Commandand all 18 remaining commands.hash()on them, and using one as asetmember ordictkey, now raisesTypeError. Equality is upstream's: floating-point fields compare within the C++ tolerance (JointStatepositions withisApprox(1e-5), for example),ContactRequest.is_validcompares the validator object,BytesResourceandSimpleLocatedResourcecheck only whether a parent is set, geometry compares by its per-instance uuid (so the same URDF parsed twice gives unequalAddSceneGraphCommands), andEnvironmentcompares its timestamps, so two environments initialised separately are unequal whileenv == 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.ResourceLocatorandResourcekeep identity equality, because their C++operator==returnstruefor any two objects.KinematicLimitschecks 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_graphexported a module constant for eachJointTypevalue (JointType_FIXED,JointType_REVOLUTE,JointType_CONTINUOUS,JointType_PRISMATIC,JointType_FLOATING,JointType_PLANAR,JointType_UNKNOWN), kept from the SWIG bindings. C++ has no such names, andJointTypeis bound as an enum. All seven are gone: importing one raisesImportError, reading one as a module attribute raisesAttributeError. 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 |
PointCloudPointrenamed toPointCloud.Point— the point type oftesseract_geometry.PointCloudis the nested structPointCloud::Pointin C++, but was bound at module level asPointCloudPoint, a name C++ does not have. It is now bound nested, andPointCloudPointand its__all__entry are gone: importing it raisesImportError, reading it as a module attribute raisesAttributeError.PointCloud.pointsis typedlist[PointCloud.Point].addPointis unchanged and is still the way to grow a cloud (pointsconverts to a fresh list on each access).Octree.updatestays unbound: upstream's body isassert(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 toMODIFY, as upstream — the binding defaultedoverride_typetoREPLACE, so leaving it out replaced every pair margin where the same C++ call merges the given pairs into the existing ones. PassCollisionMarginPairOverrideType.REPLACEto keep the old behaviour (#247).IfoptQPProblemandIfoptProblemremoved;TrajOptQPProblemreplaces them — code that buildsIfoptQPProblem(IfoptProblem(nodes))now fails withAttributeError: buildTrajOptQPProblem(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;addCostSetchecks bounds; no cost bypasses the QP; the merit matches the model for squared costs; dynamic sets are accepted.docs/breaking-changes.mdhas the migration and the five differences (#148). trajopt is removingIfoptQPProblem(trajopt#595), andIfoptProblemwas bound only to construct it. The online SQP example, the tests and the docs buildTrajOptQPProblem, as tesseract_planning's planner and itsonline_planning_example.cppdo. The example added its velocity cost to the wrappedIfoptProblem, which kept it out of the QP (onlyIfoptQPProblem.addCostSetfeeds 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, andgetTotalExactCost()reports that sum instead of the first velocity row.- The convex evaluators take the QP solution vector —
evaluateConvexCosts,evaluateTotalConvexCostandevaluateConvexConstraintViolationsraiseValueErrorunlessvar_valshas exactlygetNumQPVars()entries, the NLP variables followed by the slack variables, and before the firstconvexify(). 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 intovar_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 firstconvexify()an empty or NLP-sized vector segfaulted on a squared cost. What breaks: an NLP-sizedvar_valson 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.mdhas 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,Matrix3dorMatrix4dparameter raisedTypeErrorfor a list or tuple:Isometry3d.Identity().translate([1, 2, 3]),Isometry3d([x, y, z], q),env.setState(names, [..]),JointWaypoint(names, [..]), even the vector overload ofQuaterniond.from_rpy([r, p, y]). One caster intesseract_nb.h, seen by all 23 modules, wraps nanobind's: when nanobind refuses an argument and implicit conversion is allowed, it runs a sequence throughnumpy.asarrayand asks nanobind again, so shape, dtype and layout checks are nanobind's. A wrong shape ([1, 2]for aVector3d) still raisesTypeErrorand nothing is padded or truncated; integers convert tofloat64; strings,None, complex numbers and ragged nesting raiseTypeError, and an integer parameter (meshfaces) 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 asNDArray | Sequence[float](Sequence[Sequence[float]]for a matrix), the in-place ones asNDArrayalone; 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.setStatemoves 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 readSQPStatusand the remaining constraint violation (#12). plan_freespace,plan_omplandplan_cartesiantake acomposer— each call built a newTaskComposerand 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 throughplan_cartesian. A keyword-onlycomposer=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-onlycomposer=;test_planning_benchmarksandtest_parallel_scalinghold 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, andrun_scaling_benchmarks.shno 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.mdcalled methods removed since 0.33 andtcp_pose.position, which does not exist. Targets now come from upstream'sfreespace_example.cpp(the README drops the obstacle upstream does not have).tests/docs/test_getting_started_snippets.pyruns 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_tcpswas 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 throughSRDFModel.kinematics_information.group_tcpsandEnvironment.getKinematicsInformation(), and resolves each withfindTCPOffset. The API pages document the rule from the tesseract 0.35.0 sources:findTCPOffset's resolution order, the TCP poseT(tcp_frame) * offset, Cartesian targets relative toworking_frame, IK onC * offset⁻¹,getCombinedtakingtcp_offsetonly withtcp_frame, the SRDF's scalar-firstwxyz, andsaveToFilewriting TCPs at 6 significant digits (#133). - A Python exception keeps its type across extension modules — a Python
ConstraintSetwhosegetValuesraisedIndexErrorreached the caller ofTrustRegionSQPSolver.solve,init,TrajOptQPProblem.convexifyorgetExactCostsasRuntimeErroron macOS, with the traceback as its message. Each extension links nanobind statically and so carries its own typeinfo fornanobind::python_error; macOS libc++ compares typeinfo by pointer, so_trajopt_sqpdid 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.shsetsMACOSX_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 theirmacos-14runner. CI's wheel tags do not change (#156).- The missing
Environmentgetters bound —getInitRevision,getTimestamp,getCurrentStateTimestamp,getJointLimits,getLinkCollisionEnabled,getLinkVisibility,getLinkTransforms(all three overloads),getCurrentFloatingJointValues(both) andgetContactManagersPluginInfo. The timestamps return a naive local-timedatetime.datetime.getLinkTransforms(names, values[, floating_joints])returns the C++ out-param as adictand validates its input likesetState(ValueError). A name that is not in the scene graph raisesKeyErrorfromgetJointLimits,getLinkCollisionEnabled,getLinkVisibilityandgetCurrentFloatingJointValues(names), instead ofNoneor an arbitrarybool.getJointLimitsreturns a copy (#188). - The remaining plugin-info structs bound —
ContactManagersPluginInfo,ProfilesPluginInfoandTaskComposerPluginInfohad no binding, so nothing that takes or returns one could be bound either;KinematicsPluginInfolackedCONFIG_KEY. All four now carry a read-onlyCONFIG_KEY, and the three new classes compare by value and are unhashable (__hash__ = None), through a new sharedbind_value_equalityhelper.AddContactManagersPluginInfoCommandis bound too, with value equality, andEnvironment.applyCommandaccepts it; its getter returns a copy, so editing it cannot change a command already in an environment's history (#164). - Every
ContactResultpair field raises on a wrong-length list — assigningtype_id,link_names,shape_id,nearest_pointsortransforma 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 raisesTypeError, ascc_timeandcc_typealready did. The three pair fields that had no binding are added:subshape_id(default[-1, -1]),nearest_points_localandcc_transform, which a continuous contact test fills with the cast object's end pose (#172). ContactResultMapreadable by key, iterable and mutable — Python could only flatten a map. It now hasat(key),getContainer()(adict) and__iter__over(key, ContactResultVector)pairs, plusaddContactResult/setContactResult(single result or vector),shrinkToFit,filter(fn)andaddInterpolatedCollisionResults. Every value handed out is a copy, and iteration walks a snapshot, so no Python reference into the map can dangle after an insert orshrinkToFit; only afiltercallback gets the live vector, for the duration of the call. A missing key raisesKeyError. 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) raisesUnorderedLinkPairError, and an empty vector (whose last element upstream would read) raisesEmptyContactResultsError; both subclassValueError(#173).- Python can approve or reject contacts during a contact test —
ContactResultValidatoris bound as a subclassable class whose__call__(result) -> boolthe managers call for each contact, andContactRequest.is_validtakes one (Nonedisables it).contactTestkeeps releasing the GIL; the validator re-acquires it per call, so it works from worker threads, and an exception it raises propagates out ofcontactTest. The request keeps the validator object alive (#171). ContinuousContactManager.setCollisionObjectsTransformtakes 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 namesetCollisionObjectsTransformCast. They are now overloads of the native name too, next to the three single-pose ones, as in C++.setCollisionObjectsTransformCaststill works (#174).ContactManagersPluginFactorybinds 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 explicitPluginInfo, and read (getConfig(), a YAML string) or save (saveConfig(path)) the config. Unknown names inremove…/setDefault…raiseKeyError(upstream throws a generic runtime error), andsaveConfigraisesOSErrorwhen the file cannot be written (upstream ignores the failed write). Everycreate…ContactManageroverload 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_collisionstubs name the geometry types — the collision extension never importedtesseract_geometry, so its stub spelledGeometry,MeshandConvexMeshas quoted C++ names ("tesseract::geometry::Geometry") inaddCollisionObject,getCollisionObjectGeometriesandmakeConvexMesh, which type checkers cannot resolve. The extension now importstesseract_geometryitself, and the stubs name the Python classes (#168).createConvexHullbound; a failed hull raisesConvexHullError— Bullet's convex hull of a raw point set was reachable only throughmakeConvexMesh, which takes aMesh.createConvexHull(input, shrink=-1, shrink_clamp=-1)returns the C++ out-params with the result as(n_faces, vertices, faces), wherefacesis the flat[count, i0, …]array. Upstream signals failure only by returning a negative face count, which happens when a positiveshrinkcannot be applied; the binding raises the newConvexHullError(aRuntimeError) 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 ofConvexMeshhulls, configured byVHACDParameters(all ten fields, header defaults) and theFillModeenum under its native name;ConvexDecompositionis the abstract base, without a constructor.computereleases the GIL. Upstream reads thefacesarray without bounds checks, so the binding validates it first: a non-triangle face raisesNonTriangleFaceError, a count running past the end or an out-of-range vertex index raisesMalformedFacesError(bothValueError). The extension links tesseract'scollision_vhacdcomponent and does not compile a second V-HACD copy; a build-time#errorand annmtest (Linux, macOS) guard that (#179). - Collision margin arithmetic on the margin types —
CollisionMarginPairDataandCollisionMarginDatagainincrementMargins,scaleMargins,apply(pair_data, override_type)and the per-objectgetMaxCollisionMargin(obj);CollisionMarginPairDataalso gainsgetMaxCollisionMargin(). New constructors:CollisionMarginPairData({(a, b): margin, ...}),CollisionMarginData(default, pair_data)andCollisionMarginData(pair_data)(default margin 0). As upstream: the pair-data getters returnNonewhen no pair margin applies, whileCollisionMarginData.getMaxCollisionMargin(obj)falls back to the default margin (#178). - Every
Resourceis aResourceLocator;GeneralResourceLocatorbinds its API —Resourcewas bound without its C++ base, solocateResourcecould not be called on aBytesResourceorSimpleLocatedResource, and aBytesResourcecould not take theparentlocator that resolves its relative urls. BothBytesResourceconstructors now takeparent=None, and their arguments are named.GeneralResourceLocatorgains its two native constructors,GeneralResourceLocator(paths=[...], environment_variables=[...])andGeneralResourceLocator(environment_variables=[...]), plusaddPathandloadEnvironmentVariable. Both constructors take keyword arguments only: a positionallist[str]could be directories or environment variable names, so it raisesTypeError.locateResourceis typedResource | None(#166). JointTrajectoryandAddTrajectoryLinkCommandbound —JointTrajectoryhad 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 raisesValueError), the list protocol (len, indexing with negative indices, assignment, iteration) andpush_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 withtraj[i] = js; an index out of range, andfront/back/pop_backon an empty trajectory (undefined behaviour in C++), raiseIndexError. 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 nestedMethodenum (noMethod_*module constants), andEnvironment.applyCommandaccepts it with the GIL released;getTrajectory()returns a copy (#167).- The rest of
AllowedCollisionMatrix,makeOrderedLinkPairandgetAllowedCollisionsbound —AllowedCollisionMatrix(entries)builds an ACM from thedictthatgetAllAllowedCollisions()returns, ordering each key as upstream does. New: the single-linkremoveAllowedCollision(link_name), which drops every entry involving the link,reserveAllowedCollisionMatrix(size), andstr(acm)(C++operator<<, one line per entry). The free functionsmakeOrderedLinkPair(a, b)(atuple, smaller name first) andgetAllowedCollisions(link_names, acm_entries, remove_duplicates=True)are bound too (#180). ManipulatorInfo(manipulator, working_frame, tcp_frame, tcp_offset=Isometry3d.Identity()),emptyandgetCombinedbound — only the default constructor was bound, so Python built aManipulatorInfofield by field. As upstream:empty()isTrueunlessmanipulator,working_frameandtcp_frameare all set, andgetCombined(override)takes every non-empty field ofoverride, with an overridingtcp_framebringing itstcp_offsetalong. Settingtcp_offsetto anything but astror anIsometry3dnow raisesTypeError, in the constructor and on the field, instead ofRuntimeError: std::bad_cast; reading it still returns a copy (#181).KinematicLimits.jerk_limitsandresize,isWithinLimitsandenforceLimitsbound —jerk_limitsis a read-write(n, 2)array like the other limit fields, andresize(n)resizes all four.isWithinLimits(values, limits)checks the bounds with no tolerance (unlikesatisfiesLimits).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 newLimitsSizeMismatchError(aValueError) whenlen(values)differs from the number of limit rows (#182).- Frame and error math from
tesseract/common/utils.hbound —twistChangeRefPoint,twistChangeBase,jacobianChangeBase,jacobianChangeRefPoint,calcRotationalError,calcTransformError, all fourcalcJacobianTransformErrorDiffoverloads andapplyTolerances, under their native names. Python can now re-base acalcJacobianresult 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 andapplyTolerancesfunctions work in place, as in C++: they returnNoneand write into the array passed in, and raiseTypeErrorfor 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 newToleranceSizeMismatchError(aValueError); upstream raises a bareRuntimeErrorforapplyTolerancesand does not check the error-diff overloads (#184). - The command history readable and replayable;
CommandTypebound —CommandTypewas unbound, soCommand.getType()raisedTypeErroron every call.Environment.getCommandHistory(),applyCommands(commands)andinit(commands)(the inverse ofgetCommandHistory: the list must start with anAddSceneGraphCommand) are bound, andCommandAppliedEvent.commandsholds 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,AddTrajectoryLinkCommandincluded.SetActiveDiscreteContactManagerCommandandSetActiveContinuousContactManagerCommandare bound with value equality, andapplyCommandaccepts them. As upstream, only these commands are recorded in the history:Environment.setActiveDiscreteContactManager(name)and its continuous twin are not (#186). - Every
Environmentstate, joint-value, link-name, group and contact-manager overload under its native name —getStategains its three C++ overloads:(joints_dict, floating_joints={}),(names, values, floating_joints={})and(floating_joints).setStategainsfloating_jointson both forms and a(floating_joints)form. Also new:getCurrentJointValues(names),getActiveLinkNames(names),getStaticLinkNames(names),getJointGroup(name, joint_names), andgetDiscreteContactManager(name)/getContinuousContactManager(name), which return a copy built from any registered plugin.getStatenow validates its input likesetState(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. Afloating_jointskey that is not a floating joint raisesValueErrorfrom everygetStateandsetStateform, as it already did fromgetLinkTransforms. Upstream raisesIndexError, and insetStateit does so after storing the joint values. A name that is not an active joint raisesKeyErrorfromgetCurrentJointValues(names), which raisedIndexErrorbefore. An unknown joint raisesValueErrorfrom the link-name getters andgetJointGroup(name, joint_names). A plugin name that is not registered raisesKeyErrorfrom the by-name contact-manager getters, where C++ returns null. The by-name managers keep theirEnvironmentalive (the gh-72 rule).getStateByMap,getStateByNamesAndValues,setStateByNamesAndValuesandgetCurrentJointValuesByNamesstill work: each is bound to the same function as its native overload, so they gain the same validation and thefloating_jointsargument (#187). EnvironmentContactAllowedValidatorbound — the validatorEnvironmentinstalls on every contact manager it creates or clones came back fromgetContactAllowedValidator()as the bareContactAllowedValidatorbase, and Python could not build one. It is now bound intesseract_environmentas a subclass oftesseract_common.ContactAllowedValidator, with itsSceneGraphconstructor only: the C++ default constructor exists for serialization and leaves a null scene graph that__call__would dereference, soEnvironmentContactAllowedValidator()andEnvironmentContactAllowedValidator(None)raiseTypeError. 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) andcheckTrajectorySegmentbound — the single-step collision checks fromtesseract/environment/utils.hhad no binding. C++ fills aContactResultMap&out-param and returnsvoid; 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 withstate.at(link), so a state map missing one threwstd::out_of_rangeafter moving the links before it; the binding checks first and raisesKeyErrornaming every missing link, leaving the manager untouched (#190).cast_CommandAppliedEvent/cast_SceneStateChangedEventcheck 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 hitAttributeErrorlater. They now raise the newEventTypeError(aTypeError) naming the event'sEventsvalue and the expected one. Neither cast is needed in an event callback: the callback already receivesCommandAppliedEventorSceneStateChangedEvent, never the bareEvent. The parameter is namedevt(#191).- Python can supply TCP offsets:
Environment.addFindTCPOffsetCallbackbound —findTCPOffsetcould resolve only anIsometry3dor an SRDF group TCP, because nothing could register a find-TCP callback from Python.addFindTCPOffsetCallback(fn)takesfn(manip_info) -> Isometry3d;findTCPOffsetasks the callbacks in registration order for any other name.clone()copies them, and the environment keeps them alive. A callback takes the GIL itself, soclone()keeps running without the GIL andfindTCPOffsetworks from task-composer worker threads. As upstream, a callback that raises, or returns anything but anIsometry3d, counts as "not located": the next one is asked, and when none returns a transformfindTCPOffsetraisesRuntimeError("Could not find tcp by name …") without the callback's own exception.getFindTCPOffsetCallbacksandgetEventCallbacksstay unbound: they return C++std::functions that Python can neither inspect nor call usefully (#183). TaskComposer.get_available_pipelines()reads the factory;!includeconfigs work — the method re-parsed the config file with PyYAML, which does not know the!includetag, so any config using it raisedyaml.constructor.ConstructorError. It now takes the node names from the factory, which expanded!includewhen it loaded the config, andcomposer.pyno longer importsyaml.TaskComposerPluginFactory.getTaskComposerNodePlugins()(dict[str, PluginInfo], a copy) andgetAvailableTaskComposerNodePlugins()(the node factory class names the plugin libraries export, such asPipelineTaskFactory; not the configured names) are bound; the second loads the plugin libraries, so it pins them likecreateTaskComposerNodedoes (GH #48). ATaskComposerbuilt withoutconfig_pathnow lists the factory's pipelines instead of[];config_pathis informational (#185).- A
Nonein a list of objects raisesTypeErrorinstead of crashing — nanobind rejectsNonefor a direct object argument, but turns aNoneelement 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 theJointVelConstraint/JointAccelConstraint/JointJerkConstraintconstructors segfaulted at once, andCompoundMesh([None, None])stored null meshes. Each now raisesTypeErrornaming the argument and index ("addCollisionObject: shapes[1] is None, expected a Geometry"), through one shared check thatEnvironment.applyCommands/init(commands)already used (#201). CalibrationInfoandSRDFModel.contact_managers_plugin_infobound —SRDFModel.calibration_infowas bound but its type was not, so reading it raisedTypeErrorand the stub quoted the C++ name;SRDFModel.contact_managers_plugin_infohad no binding.tesseract_common.CalibrationInfonow hasjoints(dict[str, Isometry3d], a copy on read: assign the whole dict or useinsert),insert(other)(overwrites by joint name),clear(),empty()and a read-onlyCONFIG_KEY("calibration"). It compares by value with upstream's tolerance (each transformisApprox(1e-5)) and is unhashable.SRDFModel.contact_managers_plugin_infoholds theContactManagersPluginInfothe SRDF's<contact_managers_plugin_config>points at, andsaveToFilewrites both configs beside the SRDF (#217).- Core-module stubs name the scene-graph, state, environment and program types —
tesseract_kinematics,tesseract_serialization,tesseract_srdfandtesseract_state_solverused 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 intesseract_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 SceneStateworks as before (#218). - Every
StateSolverstate, Jacobian and link-transform overload under its native name, validated;insertSceneGraphbound —setStatetook only a full joint vector, the map and names-plus-values forms existed only assetStateByMap/setStateByNamesAndValues, no overload tookfloating_joint_values, andgetLinkTransformsandMutableStateSolver.insertSceneGraphhad no binding.setStatenow binds its four C++ overloads (vector,dict, names + values, each withfloating_joint_values={}, andsetState(floating_joint_values)),getStateits five,getJacobianits three, andgetLinkTransforms()/getLinkTransforms(names, values, floating_joint_values={})(the C++ out-param returned as adict). 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 raisesValueError; an unknown link ingetJacobianraisesKeyError. Before,OFKTStateSolverdereferenced a null node for an unknown joint name, sosetStateByMap({"nope": 1.0})crashed the interpreter (the GH #43 crash on the solver), andKDLStateSolverskipped it silently.setStateByMapandsetStateByNamesAndValuesstill work: each is bound to the same function as its native overload, so they gain the validation andfloating_joint_values. As upstream,KDLStateSolverignores floating joint values and raisesRuntimeErrorforsetState(floating_joint_values)/getState(floating_joint_values).insertSceneGraph(scene_graph, joint, prefix="")returns upstream'sbool(#219). KinematicsPluginFactorybinds 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 explicitPluginInfo, or read or save the config.KinematicsPluginFactorynow bindsadd/get/remove/setDefaultforFwdKinPluginandInvKinPlugin(get…Plugins()returns adictof group name →PluginInfoContainer),createFwdKin(solver_name, plugin_info, scene_graph, scene_state)/createInvKin(…)beside the group-name overloads,getConfig()(the YAML document asstr) andsaveConfig(file_path). As inContactManagersPluginFactory(#170):setDefault…andremove…raiseKeyErrorfor an unknown group or solver,saveConfigraisesOSError(FileNotFoundErrorfor a missing directory) where upstream ignores a failed write, and everycreate…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 newKinematicsPluginRemovalError(aRuntimeError) 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…KinPluginstill raisesRuntimeErrorfor an unknown group (#211).- Every
JointGroup.calcJacobianandKinematicGroup.calcInvKinoverload under its native name;KinematicGroupconstructor bound —calcJacobiantook only(joint_angles, link_name), thelink_pointform existed only ascalcJacobianWithPoint, and the two forms with abase_link_name(the frame the Jacobian is expressed in) were not bound;calcInvKintook aKinGroupIKInputor the opaqueKinGroupIKInputsbut not a list;KinematicGroupcould only come from anEnvironment.calcJacobiannow 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 raisesKeyErrorfor a link name the group does not have. Before, the two existing forms raised upstream'sRuntimeErrorfor 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 ofinv_kin, so the solver passed in stays usable, and keepsinv_kin(and so its factory) alive; upstream's checks raiseRuntimeError.calcJacobianWithPointandcalcInvKinMultiplestay, bound to the same function as their native overloads (#212). - Kinematics utilities bound:
numericalJacobian,calcManipulability,isNearSingularity, the harmonizers,isValidandcheckKinematics— oftesseract/kinematics/utils.honlygetRedundantSolutionswas bound. Now:numericalJacobian(three overloads: aForwardKinematicstip link, aJointGrouplink, and a link relative to a base link with offsets; the 6 × n matrix is returned),calcManipulabilitywithManipulability/ManipulabilityEllipsoid(read-only fields),isNearSingularity(jacobian, threshold=0.01),harmonizeTowardZero/harmonizeTowardMedian(in place on a float64 array),isValid(six values) andcheckKinematics(manip, tol=1e-3)fromvalidate.h, which releases the GIL. Upstream reads or writes without checking in several of them, so the bindings check first: an unknown link raisesKeyErrorand a wrong-lengthjoint_valuesValueError(numericalJacobian), a jacobian without 6 rowsTypeError(calcManipulability), an empty matrixValueError(isNearSingularity), and an index outsideqsorposition_limitsIndexErrorwithqsunchanged (the harmonizers). As upstream, a singular ellipsoid'smeasureandconditionaresys.float_info.max, andcheckKinematicsreturnsTruewhen IK finds no solution.solvePInv,dampedPInvandgetRedundantSolutionsHelperstay unbound (#163) (#213). Geometry.getUUID/setUUIDbound — every geometry carries a UUID that==compares (Geometry::operator==is type and UUID, and each subclass'soperator==starts from it), but Python could not read or set it, soBox(1, 1, 1) == Box(1, 1, 1)andgeom == geom.clone()wereFalsewith no visible reason.getUUID()returns the canonical string;setUUID(str)sets it and raisesValueErrorfor a malformed string, leaving the UUID unchanged, the spellingJointTrajectory.uuiduses.isIdenticalstill compares content (#206).- The full
Mesh,SDFMeshandPolygonMeshconstructors and theMeshTextureconstructor bound — Python could build a mesh only from vertices and faces:MeshandSDFMeshbound one(vertices, faces)constructor,PolygonMeshnone, andMeshTexturehad 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 withface_countafterfaces, with the C++ defaults (PolygonMeshalso takes a trailingtype=GeometryType.POLYGON_MESH);(vertices, faces)callers are unchanged. Upstream storesface_countwithout checking it; the binding counts the faces and raises the newMeshFaceCountError(aValueError) when they disagree, before any object exists.MeshTexture(texture_image, uvs)takes a jpg or pngResourceand the UVs;Nonefor either raisesTypeError. The API page'sMeshMaterialandMeshTextureexamples used attributes and a default constructor that never existed; they now use the bound constructors and getters (#207). ConvexMesh.CreationMethod,getCreationMethod/setCreationMethodand the fullConvexMeshconstructors bound; mesh classes compare with their ownoperator==— Python could not tell how aConvexMeshwas made, although the URDF parser recordsCONVERTEDwhentesseract:make_convexturned 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, andConvexMeshbinds both native constructors through the #207 helper, so a wrongface_countraisesMeshFaceCountError.PolygonMesh,Mesh,ConvexMeshandSDFMeshnow bind their ownoperator==instead of inheritingGeometry's type-and-UUID comparison: two meshes with the same UUID compare unequal when their vertex count, face count or scale differ (forConvexMeshalso the creation method).SDFMeshkeeps upstream's type-and-UUID comparison.ConvexMesh.clone()returns aDEFAULTmesh without normals, colors, material or textures, as upstream's does (convex_mesh.cpp:77) (#208).createMeshFromBytes,createConvexMeshFromBytesandcreateSDFMeshFromBytesbound — Python could load a mesh from a path or aResource, but not from bytes it already holds. The three instances ofcreateMeshFromBytes<T>take(url, data: bytes, scale, triangulate, flatten, normals, vertex_colors, material_and_texture); the extension ofurlselects the format. The bytes are copied, so the meshes outlivedata.triangulatedefaults toTrue, as in the bound…FromPath/…FromResourcesiblings and the URDF parser, notfalseas in the header; the other flags keep upstream'sFalse. The three…FromResourceloaders, missing fromtesseract_geometry.__all__, are added there with the new ones.createMeshFromAssetandextractMeshDatatake assimp types and stay unbound (#210).- The mesh loaders take
normals,vertex_colorsandmaterial_and_texture— upstream'screateMeshFromPath<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.daeloaded from Python had none of them, although the same file parsed as a URDF visual did. All six now take the three flags afterflatten, eachFalseby default as in C++; withflattenand all three on, a loader returns what the URDF visual path returns.triangulatekeeps itsTruedefault (#226). - Scene graph types print with upstream's
operator<<;str(JointType.X)is upstream's name —JointDynamics,JointLimits,JointSafety,JointCalibration,JointMimic,ShortestPathand theJointTypeenum have an upstreamoperator<<, but none was bound, soprint(joint.limits)showed a hand-written repr with two of six fields andprint(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", andstr(path)lists the links, joints and active joints. Changed:str(JointType.REVOLUTE)is now"Revolute"(upstream's spelling,"Unknown"forUNKNOWN), not"JointType.REVOLUTE";.nameandrepr()are unchanged, so only code that formats a joint type withstr()or an f-string sees the new text. The binding audit maps anoperator<<on an enum to<Enum>.__str__(#215). Link.visible/collision_enabled,SceneGraph.setAllowedCollisionMatrixandgetAdjacencyMapbound — Python could not hide a link or take it out of collision checking on theLinkitself, althoughclone()copies both fields and==compares them;SceneGraphhad nosetAllowedCollisionMatrixand nogetAdjacencyMap. Both fields are read/write.setAllowedCollisionMatrix(acm)keepsacmitself (later edits to it are edits to the graph's matrix, as withgetAllowedCollisionMatrix);NoneraisesTypeErrorand leaves the matrix in place, where upstream would store a null that the next ACM call dereferences.getAdjacencyMap(link_names)returns adictmapping each listed link and every link below it, stopping at another listed link, to that listed link; an unknown name raisesKeyErrorinstead of upstream'sRuntimeError.Link.getHash,SceneGraph.getVertexandgetEdgestay unbound (#163) (#216).tesseract_urdf.writeMeshToFilebound —tesseract/urdf/utils.hwas not bound at all; its one function with a Python use iswriteMeshToFile(mesh, filepath), which writes a mesh as the PLY filewriteURDFFilereferences for every mesh it exports. It always writes ASCII PLY, whatever the extension;NoneformeshraisesTypeError(upstream dereferences it unchecked), and a path it cannot write raisesRuntimeError. The urdf extension now importstesseract_geometry, so its stub namesPolygonMesh. The string helpers of the same header (toString,trailingSlash,noTrailingSlash,noLeadingSlash,makeURDFFilePath) stay unbound (#163). New API pagetesseract_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_examplebuilds IIWA kinematics from the plugin YAML withKinematicsPluginFactory(Path, locator), creates solvers by group name and byPluginInfo, switches the default inverse solver from KDL LMA to NR on upstream'srunInvKinIIWATestproblem (checking each solution with the solver's own stopping rule), adds, sets as default and removes a solver as upstream'sPluginFactorAPIUnitdoes, round-tripssaveConfig, and solves through aKinematicGroupbuilt without anEnvironment.kinematics_analysis_exampleports upstream'srunJacobianTest: every nativecalcJacobianoverload and the re-basedForwardKinematicsJacobian against all threenumericalJacobianoverloads, within a tolerance derived from the forward-difference step (δ = 1e-8: truncation plus rounding, 3.5e-6; measured 6.6e-8). It then showscalcManipulabilityandisNearSingularityat upstream's q and at the singular q = 0 (rank 5, three identical columns), the harmonizers,isValid,checkKinematics,KinematicLimitswithisWithinLimits/enforceLimits, and the pose-error and tolerance-band functions on upstream's unit-test values.state_solver_exampleports upstream's floating-joint test:OFKTStateSolver.replaceJointmakes the IIWA'sjoint_a1FLOATING at x = 1.25 m, then the floating value moves to y = 1.5 m and z = 1.5 m. Every nativesetState,getState,getJacobianandgetLinkTransformsoverload runs withfloating_joint_values, checked against aKDLStateSolverrebuilt from the scene graph after each change.insertSceneGraphattaches 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,makeConvexMeshandConvexDecompositionVHACD.computenow strip that prefix, keeping eachConvexMesh'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 newConvexHullFaceRepairError(aRuntimeError) instead of losing vertices. Not repairable from the binding: hulls that upstream builds in C++,AddTrajectoryLinkCommand's convex hull methods and URDFtesseract:make_convexmeshes, keep the corrupted faces until upstream is fixed (reported as tesseract#1382; removing the repair is tracked in #235) (#233). ConstraintSet.getJacobian()beforelinkWithVariablesraisesConstraintNotLinkedErrorinstead of crashing — every built-in trajopt_ifopt constraint sizes its Jacobian from the linked variable set without a null check, so callinggetJacobian()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 newtrajopt_ifopt.ConstraintNotLinkedError(aRuntimeError) naming the set. No other bound member reads the linked variables. A PythonConstraintSetsubclass is unaffected: its owngetJacobianreplaces 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_exampleports upstream'sbox_box_example.cppto all three discrete backends, built by name fromContactManagersPluginFactory(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 thebox_2m.plyhull. 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 theCollisionMarginData/CollisionMarginPairDataconstructors,incrementMargins,scaleMarginsandapply, each checked by a contact test. It edits the factory as upstream'sPluginFactorAPIUnitdoes, round-tripssaveConfig, and builds upstream'sTestGetAllowedCollisionsACM from a dict. A PythonContactResultValidatordrops one contact pair, and theContactResultMapis iterated and edited. A cast manager runs through the three sweep overloads ofsetCollisionObjectsTransform, reproducing upstream's cast-unitcc_transformandnearest_points_localpoints. On the IIWA environment it usesEnvironmentContactAllowedValidator, managers by name and theSetActive*ContactManagerCommandpair.trajectory_collision_exampleruns bothcheckTrajectoryStateoverloads andcheckTrajectorySegmenton 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 rebuildscheckTrajectory'sLVS_CONTINUOUSresult on upstream's IIWA-and-sphere trajectory fromcheckTrajectorySegmentandaddInterpolatedCollisionResults. Each step equalscheckTrajectory's, with upstream's 288 contacts. Equality needs one reusedContactResultMap, becauseclear()keeps its keys. Last, it builds aJointTrajectorywith both constructors and appliesAddTrajectoryLinkCommandwith eachMethod: 14, 2, 7 and 1 collision shapes, as each method's semantics predict.convex_decomposition_exampleports upstream'screate_convex_decompositiontool, with its options, ranges andVHACDParametersdefaults asargparse, plus a--fill_modeoption. It splits the car-seat mesh into 64 V-HACD hulls, checks each against its own faces with anorient3dplane test (and, for the flat hulls V-HACD can return, anorient2dpolygon test), and writes them as PLY files that read back to their written precision now that hull faces are repaired (#233). EachFillModegives its own decomposition. A secondcreateConvexHullof 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 oneRAYCAST_FILLhull loses a vertex 4 µm above a face. CartLineConstraint.getJacobian()with fewer than 6 indices raisesCartLineIndicesUnsupportedErrorinstead of corrupting memory — trajopt 0.35.0 sizes the Jacobian bylen(indices)rows but fills 6 in both the numeric and the analytic branch, soindices=[0, 1, 2]crashed most calls and could also return.CartLineConstraintnow binds its owngetJacobian: after the unlinked check (ConstraintNotLinkedErrorstill comes first), it raises the newtrajopt_ifopt.CartLineIndicesUnsupportedError(aValueError) naming the set and the index count. Construction,getValues()andcalcValues()still accept 1 to 6 indices. Direct Python calls only: aTrajOptQPProblemsolve 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.setCollisionObjectsTransformhas oneMappingoverload, and it releases the GIL — the binding had two map overloads, one taking the alignedTransformMapand one takingstd::map. Both convert the samedict, so the stub listedsetCollisionObjectsTransform(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 unreachablestd::mapoverload is removed, and theTransformMapoverload, the one that runs, now releases the GIL during the transform update.ContinuousContactManager's single-pose map overload releases it too. Adictor any otherMappingconverts 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_examplebuilds ABB IRB2400 environments withinit(Path, Path, locator)andinit(Path, locator)fromGeneralResourceLocators made by each constructor,loadEnvironmentVariableandaddPath, and checksgetCommandHistory()against upstream'sgetInitCommands. An SRDF read withinitString, carrying upstream's unit-test calibration as a fixture, adds oneChangeJointOriginper calibrated joint;CalibrationInfoandSRDFModel.contact_managers_plugin_infoare read back. On a URDF-only environment,AddContactManagersPluginInfoCommandmakes the discrete manager available,applyCommandsapplies the calibration as one batch, and an event callback receives oneCommandAppliedEventper call.init(commands)replays a history: the histories, revisions and states are equal, the environments are not, becauseEnvironment ==compares timestamps; aclone()is equal until a command is applied to it. The example spells out upstream'sSceneState ==comparison, which==now binds (#232). Last,BytesResourcefrombytesand from a list of byte values, whose parent locator resolves a relative url next to it.environment_state_examplemakes the IIWA'sjoint_a1FLOATING with aReplaceJointCommandand moves it through upstream's floating-joint origins, onesetStateoverload per step; the threegetStateand threegetLinkTransformsoverloads compute states without setting them. It reads joint values by name, the links given joints move, aJointGroupover given joints,getJointLimits, the link visibility and collision flags, the revisions, and the two timestamps, whichsetStatesets from one clock reading.findTCPOffsetresolves an explicit pose, the SRDF'slaserandweldergroup TCPs, and, afteraddFindTCPOffsetCallback, upstream's callback TCP; a link name or an unknown name raisesRuntimeError.ManipulatorInfo's full constructor,emptyandgetCombinedfollow upstream's unit test.plugin_configuration_examplebuildscontact_manager_plugins.yamlin Python as a Bullet part and an FCL part merged withinsert; the result equals what the IIWA SRDF loads, and applied withAddContactManagersPluginInfoCommandto a URDF-only environment it creates each manager by name.ProfilesPluginInfo(upstream's unit-test values) andTaskComposerPluginInfo(the shipped config's libraries, executor and two pipelines) show the merge andempty/clear; no Python constructor consumes those two yet.TaskComposerPluginFactory.getTaskComposerNodePlugins()andgetAvailableTaskComposerNodePlugins()show that every configured class is available and thatget_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'sbox_2m.plyfrom bytes withcreateMeshFromBytes(the binding'striangulate=Truedefault gives 12 triangles),createSDFMeshFromBytesandcreateConvexMeshFromBytes(triangulate=Falsekeeps the 6 quads) (#210).tesseract_material_mesh.glbgoes through eachcreate*MeshFromPathandcreate*MeshFromResourceloader withnormals,vertex_colorsandmaterial_and_textureon, giving upstream's four meshes with normals and one textured quad (#226). That quad is rebuilt through both overloads of each fullMesh,SDFMesh,PolygonMeshandConvexMeshconstructor with aMeshTexture(#207). Mesh==compares UUIDs, so the rebuilt pair is unequal untilsetUUID(#206);ConvexMesh.CreationMethodchangesConvexMesh ==(#208).writeMeshToFile(#220) writes PLY, andcreateMeshFromPathreads 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_directionsequaltf * prow by row, andPointCloud.Point(#209) fills aPointCloudforcreateOctree. The Basic Examples tip for loading meshes called aMesh.fromFilethat was never bound; it now usescreateMeshFromPath. Wheels now ship the support meshtesseract_material_mesh.glb: the support-data install rule copied files by an extension list that had no*.glb. scene_graph_examplebuilds a scene graph directly — a second, low-level part builds upstream's five-link test graph withSceneGraph,LinkandJoint(one joint perJointTypemember, #214), and checks it against upstream's unit tests: adjacent links, children,getAdjacencyMap,isAcyclic/isTreethrough a disconnected link and a second parent, and thestr()ofJointType, the joint value types and aShortestPath, each upstream'soperator<<(#215).setAllowedCollisionMatrixreplaces the matrix and the graph keeps the object;Link.visible/collision_enabledare the flagssetLinkVisibility/setLinkCollisionEnabledset (#216). The example had no test; it now has one.SceneState.floating_joints,getFloatingJointValuesand==bound; the audit compares members of classes bound in another module — since #219 everyStateSolver.setState/getStateoverload takesfloating_joint_values, but the returnedSceneStatehad nofloating_joints, so Python could set floating joint values and not read them back.floating_jointsis now adict[str, Isometry3d]likelink_transforms(a copy on read, assigned as a whole),getFloatingJointValues(joint_names)returns the named values and raisesKeyErrorfor a name that is not a floating joint (upstream throwsstd::out_of_range), and==/!=are upstream's comparison: exact joint values, transforms withinisApprox(1e-5);SceneStateis unhashable. The binding audit missed the gap becauseSceneStateis declared in a scene_graph header and bound intesseract_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.getJointChildrenNamestakes a list of joint names — only the one-name overload was bound, sogetJointChildrenNames(["joint_1", "joint_2"])raisedTypeError. The list overload returns the links below any of the joints, sorted and without duplicates, as upstream (graph.cpp:909–919);Environmentcalls it with the active joint names. Astrstill 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 boundgetJointChildrenNames(str).scripts/audit_types.pynow maps each C++ parameter type and each stub annotation to a key (std::vector<T>andSequence[T],std::filesystem::pathandstr | os.PathLike,Eigen::VectorXdandNDArray[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 inconstofthisor&/&&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 newtype fallbackscolumn counts those overloads.ContactResultMap.addInterpolatedCollisionResultsis no longer misread as returning its in/outsub_segment_results. The 9 overloads the arity rule hid are bound or accepted: the plugin factories'YAML::Nodeconstructors (ruleyaml-node: the path and YAML-text overloads load the same config) andCompoundMesh'sConvexMesh/Mesh/SDFMeshlist constructors (rulederived-element-container: upstream upcasts them into thePolygonMeshone, 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_typedefaults toMODIFY, as upstream (#247).CompoundMesh==compares its meshes —==fell through toGeometry.__eq__, which compares type and UUID only, so two compound meshes with different meshes and one UUID compared equal.==/!=are now upstream'sCompoundMesh::operator==, which also compares every mesh (#247).OFKTStateSolver(root_name)— a solver holding the root link only, grown withaddLink/moveLink(#247).Isometry3d.apply_pointsandapply_directions: a batched apply —iso.apply_points(points)maps an(N, 3)float64 array in one call, where a Python loop overiso * pwas the only way: about 126× slower than numpy at 2 000 points (osx-arm64, 0.35.0.8). Rowiequalsiso * points[i]bit for bit, because each row runs the same fixed-size Eigen product; a dynamic3 × Nproduct vectorises and fuses differently and came out up to 4e-16 apart.apply_directionsapplies the rotation only, asiso * vwould with the translation zeroed. Both takeN = 0and strided views without a copy, convert Fortran-ordered and non-float64 input, and return a new array; a(3,)array raisesTypeError(a single point staysiso * 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'sp @ R.T + t. TheIsometry3ddocs saidtranslation(),rotation()andmatrix()were methods: they are read-only properties.TrajOptQPProblembound — 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 noIfoptProblemlayer. 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 onlyIfoptQPProblem),IfoptQPProblemdoes 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 anABSOLUTEorHINGEcost 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 withcoeffRefwhile 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 unlessrange_bound_handling=KEEP_AS_IS. In trajopt 0.35.0, which every 0.35.0.x wheel bundles, the range split indexes the caller'scoeffsargument instead of the broadcast membercoeffs_, so a length-0/1coeffsis 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
ConstraintSetsubclasses reportgetNonZeros() == 0— the Jacobian non-zero hint kept trajopt's unset default of −1, which Python could not change.TrajOptQPProblemreserves storage from the sum of its cost hints, so a Python cost set on its own raisedValueError: vector. 0 is a valid hint: storage grows as needed. SQPCallbackreceives the problem itself, not a copy — nanobind's defaultautomatic_referencepolicy 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 tosolve);resultsis 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, raisedRuntimeError: LVSDiscreteCollisionEvaluator, should be configured with LVS_DISCRETE, because the example never set the collision check type.online_planning_example.cppsetsDISCRETEfor both modes, so its continuous mode throws the same way. The example now setsLVS_DISCRETEthere. As the C++ reference does, it also sizes every collision set by the config'smax_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
TrajOptQPProblemexample: 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. Thetrajopt_ifoptreference now lists, for each constraint class, its row bounds and the penalty typesTrajOptQPProblemaccepts 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. TaskComposerPluginFactoryaccepts anyResourceLocator— the constructor andcreateTaskComposerPluginFactoryraisedTypeError: locator must be a GeneralResourceLocatorfor every other locator, including PythonResourceLocatorsubclasses, so a model locator could not be kept apart from the installation one. Both now bind theResourceLocatorbase, as the C++ constructor does (#150).TesseractViewer.close()shuts the server down — it called a_TesseractViewerAIOServer.close()that did not exist, printed theAttributeErrorand 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._attachasserts the server is open; the accepted socket leaks inCLOSE_WAIT), so the viewer owns itsasyncio.Serverand 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).applyCommandreleases the GIL for the two commands that carry geometry — adding a link whose collision geometry is a mesh spends seconds insideEnvironment::applyCommandbuilding collision shapes, all of it holding the GIL, so every other Python thread stalled on what is pure C++ work.applyCommand(AddLinkCommand)andapplyCommand(AddSceneGraphCommand)now drop the GIL around the C++ call, matching thesolve()/contactTestguards, as dogetDiscreteContactManager/getContinuousContactManager—applyCommandonly updates a contact manager that is already cached, so with a cold cache the shape build lands in the first getter call instead.EventCallbackFnnow 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-releasingEnvironmentmethod 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::cloneholds the environment's shared lock (serialised againstsetState/applyCommand), and the copy touches no Python state — event callbacks are not cloned,find_tcp_cbhas no Python binding, and Python-owned resources are onlyshared_ptr-copied. A private_GilProbe, attached as a find-TCP callback thatclone()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_profilesdocuments the realsample_maxdefault — 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 changessample_resolutionsetssample_maxwith it.- Typing stubs match the bindings; CI fails on drift — the committed
.pyistubs had not been regenerated since the 0.35 namespace change: they still spelledtesseract_common::…, lackedContinuousContactManager.getCollisionObjectGeometries/getCollisionObjectGeometriesTransforms(#157), and had no stub at all fortesseract_serialization. All stubs are regenerated.pixi run stubsregenerates them andpixi run stubs-checkfails 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.waitForraisedTypeErroron return; it now returns the newFutureStatusenum (ready/timeout/deferred).Resource.getResourceContentStreamraisedTypeErrortoo; it now returns the contents as anio.BytesIO.- Tesseract maps that use
Eigen::aligned_allocator(e.g.TransformMap) had no Python conversion, because nanobind'sunordered_mapcaster matches default-allocator maps only. They now convert to and fromdict. KinematicsPluginFactoryfoundFilesystemPathonly iftesseract_commonhad 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()andTaskComposer.plan_cartesian()now select the registered, double-precisionDescartesDPipeline; the previousDescartesPipelinename 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.messagenow reports the failed node and its specific diagnostic, such as a TrajOpt iteration limit or a failed contact check, instead of hiding it behindErrorTask: 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
libompruntime 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 —
TrajOptIfoptDefaultCompositeProfileexposed only smoothing andTrajOptIfoptSolverProfileonly 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) andopt_params(trajopt_sqp::SQPParameters) are now bound.opt_paramssits on the base solver profile —create()assigns it straight onto theTrustRegionSQPSolver, making it the only route tomax_iterations(50),max_qp_solver_failures(3), the trust-region ratios, and merit-coeff inflation; the sco back-end has always exposed these viaTrajOptOSQPSolverProfile, so an IFOPT migration silently lost them. The collision configs carrymax_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_ifoptre-exports the types its profiles need —TrajOptCollisionConfig,SQPParameters, andCollisionEvaluatorTypeare re-exported from the planner module, matchingtesseract_motion_planners_trajopt, so configuring a composite or solver profile no longer means importing fromtrajopt_ifopt/trajopt_sqp/tesseract_collisionalongside it. The module's__all__also regainsTrajOptIfoptMoveProfile,TrajOptIfoptDefaultMoveProfile, andProfileDictionary_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 andRangeBoundHandlingdo.- TrajOpt factory collision margins —
create_trajopt_default_profiles()andcreate_freespace_pipeline_profiles()carried C++car_seat_example.cpp's collision buffers and coefficients but not its margins, sodefault_marginstayedNoneand 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
TrajOptIfoptTaskruns, 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.libis 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_marginontocollision_margin_buffer, which trajopt_common documents as "not used when calculating the error"; it maps ontocontact_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
ProgramBackendABC, so a new controller language is a backend, not a fork. Five dialects emit from one tesseractCompositeInstruction— ABB RAPID, KUKA KRL, Fanuc LS (TP ASCII), Yaskawa Motoman JBI (INFORM), and Universal Robots URScript — all exported fromtesseract_robotics.emitterswith a mermaid architecture page. RAPID is reseated onto the core IR and decomposed intoprofile/targets/emit/backendmodules; 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 — RAPIDrobtarget/jointtargetnow carry real external-axis values (linear track in mm, rotary positioner in deg) instead of the9E9sentinel, 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=Nonestays 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/createFwdKinteardown SIGSEGV fixed — the returned solver holds raw pointers into the scene graph, scene state, and plugin factory, sonb::keep_alivebinds 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+KinematicsPluginInfobound — 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(plusFKandsetCollisionObjectsTransform) — mirrors the collision and motion-plannercall_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¶
contactTestreleases the GIL — parallel collision checking across cores —DiscreteContactManager.contactTestandContinuousContactManager.contactTestnow bind withnb::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/getContinuousContactManagernow bind withnb::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 "calcJacobiansegfaults under pytest but works in plain Python" was this lottery, not the jacobian path (lldb forensics: dangling vptr pinned inside unloadedlibtesseract_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.6into every wheel; numpy/scipy load the system copy, the loader unifiesSTB_GNU_UNIQUElocale statics across the two, andstd::regexinsidePropertyTree::rebuildAutoValidatorsfreed a stack address duringEnvironment.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 #35RTLD_GLOBALpreload is removed (its "two copies coexist safely" premise was falsified), and a test-wheel CI guard asserts ships-no-copy / one-copy-mapped / numpy-firstenv.initclean. 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 upstreammotion_planners/examples/raster_example.cpp: a 3-pass raster program solved byDescartesMotionPlannerDdirectly (no TaskComposer) with TrajOpt refinement chained on the result;CompositeInstructiongains 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 standaloneDescartesF/D(NPC)Pipelinefamily, andcreate_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_solveroverride,use_redundant_joint_solutions, andmove_profile=injection of custom (Python-subclassable, #83)DescartesMoveProfileDinstances — mutually exclusive with the knobs, fails loud (#85). - Lowlevel planning example: TrajOpt profiles reach the solver —
tesseract_planning_lowlevel_c_api_example.pyadded its TrajOpt plan/composite profiles to the OMPL dictionary while handing the solver an emptytrajopt_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 viaTESSERACT_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 newTaskComposerConfigNotFoundErrorwhen nothing resolves; the hand-rolled fallback chains in the task composer tests,TaskComposer.from_config, andscripts/generate_pipeline_dotgraphs.pynow delegate to it (#110).- ruff enforces the py39 floor —
FA102added to lint select so PEP 604 unions withoutfrom __future__ import annotationsfail the hooks; withtarget-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 brokeenvironment_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 —
OSQPEigenSolvergains seven tunable setters (setPolish,setWarmStart,setAdaptiveRho,setMaxIteration,setAbsoluteTolerance,setRelativeTolerance,setVerbosity) andTrajOptIfoptOSQPSolverProfilegainssetAdaptiveRhoInterval, enabling iteration-based (deterministic) rho adaptation for reproducible SQP solves; fixes dangling-pointer segfault ingetJacobian()viarv_policy::reference_internalongetVar/getNode/getNodes(0f40048, c984044, c083f5e, #103). - OMPL RNG determinism —
RNG_setSeed/RNG_getSeedbound intesseract_motion_planners_ompl;conftest.pyauto-reseeds per test for xdist-safe reproducibility; single-planner +optimize=Falserecipe documented as the only guaranteeable determinism contract (parallel-plan best-of-N races even with seeded samplers); Windows determinism test dropped — conda-forgeompl.libembeds a separateRNGSeedGeneratorcopy 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 buildreplacespixi run build-cpp/ colcon;pixi.lockv7 with eigen-abi-taggedorocos-kdl _2ending heap corruption in all KDL paths;locked:true+ pinnedpixi-versionin CI so stale-lock drift fails loud; build-dir cache keyed onpixi.lockhash; Python 3.9 feature env added for Rhino 8 (b6a14c4, 3c80a25, 6210a99, 0455e34). - Linux libstdc++ preload — bundled gcc-14
libstdc++loadedRTLD_GLOBALas 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 onCONDA_PREFIXabsence to avoid double-loading inside pixi/conda envs (#35, ee83dc8, 7715422). Environment.setStateinput validation — unknown or non-active (fixed/mimic) joint names now raiseValueErrorwith a clear message instead of silently dereferencing unmapped entries and crashing withSIGSEGV; all three entry points covered (dict, names+values,setStateByNamesAndValues) (#43, ca620d3).SceneGraphtype registration in URDF module —_tesseract_urdfnow imports_tesseract_scene_graphat module load so the return type ofparseURDFFileis 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 —
TaskComposerpipeline diagrams woven into docs;clean_dot_labelsstrips 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_scmlocal_scheme = "no-local-version"so a dirty tag build can't produce a PyPI-rejected local version;skip-existing: trueon all three publish workflows so re-runs fill gaps instead of 400-aborting; macOSfail-fast: falseso one flaky build can't cancel sibling wheels; plus a 3× retry on the macOSupload-artifactstep working around GitHub's long-standing arm64-runnerCreateArtifacttimeout (actions/upload-artifact#569, still open) that otherwise skips the entire macOS publish ~80% of the time (49f1aa9, 804d5b8). cp312-abi3support verified on all platforms — macOS and Windowstest-wheelnow install thecp312-abi3wheel 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+ onaarch64; Linux wheel CI standardised onpixi 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
MoveProfilecontact-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/Isometry3dsurface, with scalar-last[qx, qy, qz, qw]Quaterniond.from_xyzwandQuaterniond.from_rpy/to_rpyZYX ROS RPY interop; float64 default precision centralised via EigenNumTraits(#76, 492ab7a, 544f7dd, 1d6f5c7, ef26fc9). - ABB RAPID code emitter for
tesseract_command_language(tesseract_robotics.emitters.rapid) (#75, 37f21b2). Octreegeometry bindings (plusoctomap::OcTree,OctreeSubType,PointCloud,createOctree) and the remainingtesseract_geometryutilities (isIdentical,extractVertices,toTriangleMesh) (#67, 4575aa9, @Joelkang).- Python-subclassable Descartes evaluators/sampler/profile for
tesseract_motion_planners_descartes— new bindings forDescartesStateD,DescartesStateSampleD,DescartesEdgeEvaluatorD,DescartesWaypointSamplerD,DescartesStateEvaluatorD, and a now-subclassableDescartesMoveProfileDlet users overridecreateWaypointSampler/createEdgeEvaluator/createStateEvaluatorfrom Python (#83, 620b228, @Joelkang). - Upgraded to tesseract 0.35.0 — deps bumped (
rosinstall→repos),MIGRATION.mdsed applied, namespace aliases added, resource URIs and dev paths updated for the 0.35 layout, wheel data dirtesseract_support→tesseract/support,kinematics_core_factories→kinematics_factories, descartestp::MoveInstructionPoly→tcl::MoveInstructionPoly(#86, 6710ccf, 422c5ea, 4e08a36). Posenow subclassesIsometry3d(tesseract_robotics.planning) — rotation math via Eigen, numpy-property facade dropped, factories collapsed to one name accepting both scalar-positional and arraylike call styles,Transformalias removed in favour of the single canonicalPose, 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 toQuaterniond.from_xyzw(0bd408d, f7b2fea). - README gains an examples section and a refreshed platform-support matrix (d7bde99, 8eed899).
KinematicsInformation.group_tcpsexposed to Python (tesseract_srdf) (#79, 52fdfba).TaskComposerNodeInfoContainerreturnsTaskComposerNodeInfo(#81, 106d162, @Joelkang).getSeed()exposed onCartesianWaypoint(#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_languageinstruction types bound:WaitInstruction,TimerInstruction,SetAnalogInstruction,SetDigitalInstruction,SetToolInstruction, with theirWaitInstructionType/TimerInstructionTypeenums andInstructionPoly_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. Newtests/tesseract_command_language/test_command_language.pycovers construction, casting, andInstructionPolyround-tripping (#70, a4c26df, @Joelkang). - TaskComposer DAG visualisation —
getDotgraph()/saveDotgraph()overloads onTaskComposerNode, with an optionalTaskComposerNodeInfoContainerargument to highlight the executed path on a previously-run DAG. Emits GraphViz DOT for debugging otherwise-opaque planning pipelines (#63, e7c3a92, @Joelkang). TaskComposerDataStorageAnyPoly cast/wrap helpers —AnyPoly_wrap_TaskComposerDataStorageandAnyPoly_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).DiscreteContactCheckTaskexposescontact_resultsthroughtask_infosdata_storage, with unit and integration test coverage (#66, 4fe07bf, 06178f0, @seonixx).- SimplePlanner and time-parameterization profiles now reachable via the public
tesseract_motion_planners_simpleandtesseract_time_parameterizationpackage paths instead of only the underscore-prefixed C++ extension modules. C++ binding-side registrations retired in favour of__init__.pyre-exports;test_constant_tcp.pyconsolidated intotest_time_parameterization.py(#62, d73e227, 30c8e9f, @Joelkang). InstructionPoly_as_CompositeInstructionre-exported fromtesseract_command_languagepackage 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.ymlworkflow. Mirrorswheels-{linux,macos}.yml; sources C++ deps from conda-forge via pixi; bundles plugin DLLs viadelvewheel repair --analyze-existing(f9c122d, 3e3a2e4). __init__.pyprepends the package dir andtesseract_robotics_nanobind.libs/toPATHon Windows — required becauseboost::dll's plainLoadLibraryignoresos.add_dll_directory(006ce38).- Cereal polymorphic types re-registered in the
tesseract_serializationbinding TU — MSVC instantiates the cereal registry per-DLL, so upstream's registrations don't reach the consumer.pydwithout re-registration (af88515). - New developer doc
docs/developer/windows-wheels.mdcapturing four Windows wheel packaging gotchas surfaced during the gh-40 diagnosis (fe5ad75). CompositeInstruction.push_backnow accepts aCompositeInstructionin addition to a plainInstruction, 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 —
SimplePlannerCompositeProfileplus seven concrete move profiles (FixedSize,FixedSizeAssign,FixedSizeAssignNoIK,LVS,LVSNoIK,LVSAssign,LVSAssignNoIK), withProfileDictionary_addSimplePlannerMoveProfile/…CompositeProfilehelpers for registration (#54, e2bf837, @Joelkang). - task_composer planning-node profiles exposed through a new
tesseract_task_composer_planningmodule:ContactCheckProfile,FixStateBoundsProfile,FixStateCollisionProfile,KinematicLimitsCheckProfile,MinLengthProfile,ProfileSwitchProfile, andUpsampleTrajectoryProfile. Heavy TrajOpt-typed members onFixStateCollisionProfile(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 itsConstantTCPSpeedCompositeProfile(#55, f4f981d, @Joelkang).- vcpkg-based
wheels-windows.ymlvariant (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.ConstraintSetso 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-wheeltask 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).