Changelog¶
All notable changes to this project are documented here. The format follows Keep a Changelog, and versions follow Semantic Versioning (0.x: the API may change between minor versions).
Unreleased¶
Added¶
control.TensionFloor: an output stage for tendons that only pull. It lifts the torques of each group of tendons (a segment’s three) together until the lowest is at a floor, so no tendon is told to push. Equal torques on evenly spaced tendons only squeeze the segment, so the bend that the command makes is the same. Its corner is rounded, andsymbolicis the same map for a plan (Problem(output=)).Mechanism.motorand theMotorAnglecoordinate: entries of the robot’s motor angles, as its actuation gives them. ALimitSpringon it is a limit on how far a motor may turn.helyx.tension_stage,helyx.unwind_limitand their numbersTENSION_FLOOR,UNWIND_LIMITandUNWIND_STIFFNESS: the tension floor, and a limit on how far a motor may unwind (pulling is free). Both are optional, andhelyx.output_stageis unchanged.
1.2.2 - 2026-10-11¶
A patch: a fit says when its data do not determine a Param, and gives the same values on every machine; two error messages say what to do instead. Nothing else changes.
Fixed¶
fit_params: a Param that the data do not determine (a run that never excites it, or an entry of a matrix that only appears in a sum with another, such as the two off-diagonal entries of a spring’s stiffness) hasstdinf. It was0.0, which read as certainty. Along such a direction the fit used to end wherever the rounding of the machine left it (the difference of those two entries came out as 4.06, 3.67 or -2.31 on different CPUs); it now stays at the start, inside the bounds and only if the fit stays as good. The values and the standard errors of the Params that the data do determine are the same as before.
Changed¶
An unknown geometry in
helyx.arm,helyx.modelorhelyx.hardwareraises aKeyErrorthat names it and lists the known geometries (it was the bare name).The
ValueErrorthat a law or an estimator raises for aStateControllernow says what to use instead:underactuated.controller(compiled, "frozen")withoutgravity=True(that one reads the motors), orDirectionalForcefor a force. It used to say “the plain frozen controller”, which agravity=Truecontroller is not.
1.2.1 - 2026-10-10¶
A patch: a Ctrl-C can no longer lose the log of a real-time run. Nothing else changes.
Fixed¶
A Ctrl-C (or
_thread.interrupt_main()) that arrived in the middle of a real-time step could land inside a call into CasADi or numpy, come out as aSystemErrorand lose the log. On the main thread, with Python’s own handler on SIGINT,sim.runnow only raises a flag, reads it between two steps and returns the log; a second Ctrl-C raises at once, for a run that is stuck. A handler of your own, and any other thread, are left alone.
1.2.0 - 2026-10-09¶
A robot that runs for hours: the log of a real-time run can keep only its last seconds. Nothing that worked before changes.
Added¶
sim.run(..., window=): on real time, a run that goes on for hours keeps only the lastwindowseconds of its log (up to a quarter more, until the next trim) instead of every step in memory;info["steps"]still counts them all, and a Ctrl-C keeps only whole steps.RunLog.crop(t_min, t_max): the steps of a time window as a new log, with the sameinfoandmeta; an end left out is open.
1.1.0 - 2026-10-09¶
Static friction of the motors: a model of it, a compensation for it, its identification and a tutorial, all off unless asked for. Nothing that worked before changes.
Added¶
Static friction of the motors, off by default.
StaticFriction(breakaway, kinetic, width)[N·m] is the torque that friction takes from a motor as a smooth function of the torque applied to it: all of it below the breakaway torque F (the motor is stuck), the kinetic torque Fc with the sign of the command past it (Fc = 0: static friction gone once it moves; Fc = F: it stays), smooth (C^∞) overwidth, so solvers differentiate it. It depends on the torque only, not on the speed.Efficiency(c1, ..., friction=StaticFriction(...))takes it before the polynomial, so every transmission carries it (Direct,Underactuated,TendonTransmission, stacked ones, and configuration files), and the simulator,compile_dynamics,Collocation,Shooting,MPC, the Kalman filter and the force law all have it. Its Params arefriction.breakaway,friction.kineticandfriction.width; with F = Fc = 0, the default, nothing changes. Not passive by construction: when a command brakes the motion it can create up to F |θ̇| of power, which the tutorial states and measures.control.StaticFrictionCompensation(kinetic, width, fraction)(and.of(friction, fraction)), an output stage that adds the kinetic friction torque in the direction of the command, smooth in the command. Against aStaticFriction(F, Fc)it delivers the command once the motor moves and narrows the dead band from F to F − Fc. Nothing turns it on but you: no template or controller has it by default. It has asymbolicform, soProblem(system, output=[...])andsim.rollout(..., output=[...])apply it in a plan asVMCController(output=)does in a run (Problemnow raises for an output stage that has nosymbolicform).Identification of the friction: its Params are the robot’s, so
identification.fit_paramsfits them, with the stiffness and damping, to a run in which the command rises slowly from rest;fit_efficiency(commanded, delivered, friction=True, width=)fits the efficiency and the friction to static torques (per motor orshared), by a grid and a refinement over the breakaway and the kinetic torque.robots.adapt.finger_friction()andhand_friction(): the lab’s friction values of the finger (0.20 N·m) and the hand (0.10 N·m) as aStaticFriction, to ask for.Docs: the tutorial “Static friction of the motors”.
1.0.0 - 2026-10-07¶
The first stable release. It also carries the work meant for 0.6.0 (planning with virtual states,
unmeasured coordinates, custom dynamics, the planner on a larger plant, MPC and underactuation) and
for 0.7.0 (models as numpy and PyTorch code), which were never released on their own, and it adds
the checking: the whole suite on the oldest and the newest supported Python and numpy, on Linux,
macOS and Windows, every page of the documentation run from a clean checkout, and a review of the
public API (what __all__ lists in each module is the API) and of every page of the docs at desktop
and phone width. The rules for what changes between versions are on the page “Support policy”.
Added¶
optimization.Shooting(q0, horizon, nodes, v0=, steps=, initial=, transition=, scales=, integrator=, substeps=): multiple shooting, a second transcription next toCollocationwith the same terms. The unknowns are q and v at the nodes; over each interval the closed loop is simulated asvmc.sim.rolloutdoes (the command held over each ofsubstepscontrol steps, the robot advanced by the linearly implicit step or by"rk4"), and constraints join the end of an interval to the next node. A plan is the closed loop of a simulation at that control step (a test reproducesrolloutto rounding error), and agrees withCollocationas the control step shrinks (first order). The start is the pair of Paramsshooting.q0andshooting.v0, which a problem can declare as parameters.stepsnames live Params that take one value in every interval, between their bounds: trajectories of stiffness and references, returned inResult.steps(apply(target, interval=k)sets the values of interval k). The model does not carry the force that balances the robot at q0 underinitial, asCollocationdoes.optimization.TankBudget(level, refill=True): the planned changes of the Params that step stay within the energy of avmc.control.Tank. The plan keeps the tank’s level from the exact jump of the controller’s energy at each change, and from the energy the controller’s dampers take back, and keeps it at least zero; a test runs the plan through a realTankand finds its level to rounding error. The tank’s capacity is not planned.optimization.MPC(problem, shift=1, rti=False): the plan as a receding-horizon controller. The program is built once, with the measured state, the controller’s Params now, the tank’s level and the references as parameters of each solve.step(controller, q, v, references, latency=)solves from the measured state, starting from the previous plan moved up byshiftintervals, and applies the plan’s Params throughcontroller.set(so aTankbounds them), returning the energy jump as the adaptation laws do. A plan that tooklatencyseconds (the measured solve time by default) is applied as of that time. Withrti, one SQP iteration per step.startandpollplan in a thread and apply the plan once it is ready, as of the time since the start (CasADi lets other threads run during a solve; the solver is created with theMPC, as it is not safe to create one in a second thread).Problem.warm_up()builds the program and the solver now, andProblem.solveattaches the iteration callback only when it is given aprogress.Planning with virtual states:
Collocation,ShootingandEquilibriumaccept controllers with virtual states (a flywheel, a mass the robot is tied to). The states are unknowns of the plan at every node, with the controller’s own dynamics: collocated with the same scheme as the robot (Collocation), or advanced asVMCController.stepandvmc.sim.rolloutadvance them (Shooting, which then reproducesrolloutto rounding error, the states included). They start from the state the controller is compiled with, or fromz0(positions, then velocities); a periodic motion repeats them;Equilibriumrests them.Result.zholds them,warm_starttakes them,TankBudgetreads the energy at them, and the controller in place (initial) may have states too, as a system (it starts with the plan) or as a running controller (its state is where it is now).MPCplans a running controller fromcontroller.zwhen theShootinghas az0, which is then a parameter of the program (shooting.z0). The docstring ofShootingnow says what the code does: the controller is called at every one of an interval’ssubstepscontrol steps.Sensors of combinations of the coordinates:
Measurement(..., matrix=)takes an (m, n) matrix and readsmatrix @ qandmatrix @ v(instead ofobserved, a selection of coordinates).KalmanFilter.encoderof a robot with fewer motors than coordinates (Underactuated) no longer reads the motors as if they gaveq, with the passive coordinates frozen: it returns the measurement of the motors themselves, with the Jacobian of the motor angles at the estimate as the matrix, so the passive coordinates are left to the model (RqandRvare then the covariances of the motors’ readings). A robot with a motor on every coordinate keeps its encoder as it was. The underactuated tutorial runs the naive controller on such an estimate, and shows what it takes: a process noise that admits the model’s error (with the default one the gate throws out every reading after the first swing and the filter breaks) and a controller that does not outrun the filter.Equations of motion from a function:
models.Equations(residual, energy=None), given toFunctionModel(..., equations=), for a black-box, learned or external model whose dynamics are not built from parts. The residualr(q, v, a, tau, f, p) = M(q) a + h(q, v) - tau - fis written with CasADi operations and affine ina;tauis the generalized force of the motors andfthat of the robot’s own components, which still add springs, dampers and contact (the masses and the weight are in the equations, so there is noInertanceorGravity). Everything that runs on a robot runs on it: the simulator,rollout, the planners, the Kalman filter, the momentum observer. With anenergy(q, v, p)returning (T, V) it has the robot’s energy and power; without oneDynamics.energyandpowerareNoneand what needs them (ModelPlant.energy, thePassivationstage) raises aValueErrorthat says so (dynamics.needs_energy).testing.check_modelchecks such a robot’s mass matrix (symmetric, positive) and, with an energy, that a simulation never gains any. A test builds the same two-link arm from parts and from textbook equations and finds the same dynamics, energy, simulation, Kalman prediction and plan.Plan a controller on some coordinates of a larger robot:
Problem(system, plant=robot)takes the dynamics fromplant(every term andq0in its coordinates) while the controller stays written for its own robot, and reads the plant’s motors asVMCControllerdoes in a simulation, through its transmission. The law, the energy of aTankBudgetand the dampers’ power of a shooting all go through it. A shooting of the turtle’s flywheel controller onturtle.crawler()equals the simulation step for step, the flywheel’s states included (a test), and the tutorial “Crawl with a flywheel” plans a stride with it.Shooting(running=)says whether a controller started fromz0is already running (z0alone still means so) or starts with the plan, as one reset there does.Space.on_manifold(q)gives the equations that a configuration obeys (the unit norm of a quaternion; none for a flat space), andShootingandEquilibriumconstrain their nodes with them: before, a plan of a floating body could drift off unit quaternions and plan another body’s motion.The models as numpy and PyTorch code:
core.backends.source,translateandexportwrite a CasADi function as straight-line Python, one statement per operation of its expression graph, for numpy (the text imports numpy only, so it runs without CasADi) or PyTorch (differentiable, on any dtype and device). Arguments and results have CasADi’s shapes, with any leading batch dimensions.Kinematics.functions(at, backend="numpy"),Dynamics.exportandCompiled.exportgive a model’s functions that way, each with itssource. numpy gives CasADi’s numbers to rounding error and PyTorch to 1e-12 on the kinematics with their Jacobians and Hessians, including the derivatives by autograd; a model with a poorly conditioned mass matrix (the soft arm’s dynamics) differs by a few parts in 1e11, as PyTorch’ssinandcosdiffer in the last place. The extravirtualmodelcontrol[torch]and the tutorial “Use a model outside CasADi”.ForceTracking,StiffnessTracking,PositionRegulation,HoldingGoals,ContactForceandTaskStiffnessrefuse aStateController(also inside aTank) with aValueErrorthat says so, instead of failing on the shapes of its inputs: they read a controller in the layout of the motors. A robot with fewer motors than coordinates uses the plain frozen controller (it reads the motors), orcontrol.underactuated.DirectionalForcefor a force.Solver presets:
Problem(system, solver=...)takes"ipopt"(the default),"ipopt-exact"(exact Hessians: 40 iterations instead of 165 on the tutorial’s shooting),"sqp"(CasADi’ssqpmethodwithqrqp, the Hessian’s negative eigenvalues clipped; other QP solvers by theqpsoloption),"rti"and"fatrop"(as a general problem, without stages).optimization.PRESETSlists them andoptimization.solver.available(name)tells whether the CasADi build has the plugin.Result.statuswords FATROP’s endings as IPOPT’s.Docs: tutorial “Model predictive control”.
Docs: the page “Support policy”: which versions of Python and numpy are supported, what semantic versioning means here (a deprecation lasts at least one minor version), what the public API is, the schema of the run logs, where to report an issue, and the safety checks before the first run on a robot (also in “Real time and the robot”).
Changed¶
__all__inmath,testing,compiler,dynamicsandsystem: what they list is the public API, as in every other module;from virtualmodelcontrol.x import *gives only that.The configuration and hardware profile files are read and written as UTF-8 on every platform, so that a file written on Linux reads on Windows.
The docs use American spelling, the figure legends are 18 pt (readable on a phone), and wide tables wrap their code on a narrow screen.
0.5.0 - 2026-10-06¶
This release also carries the work meant for 0.4.0 (optimization of virtual mechanisms, the energy tank, tuning and the calibrations), which was never released on its own.
Added¶
Underactuated Virtual Model Control, for robots with fewer motors than coordinates.
models.Underactuated(B)is the actuation with an input matrix B (Underactuated.jointsfor motors on some joints): motor angles Bᵀq, torques B u, the allocation B⁺ τ, the projector E and thedefectE τ it cannot realize. Its motors give the frozen configuration (the motors’ part of q, the rest atq_rest), so aVMCControlleron it is the frozen controller.control.StateControllerreads (q, v) from the measurement (meas["q"],meas["v"], or itsread) instead of the motors, which is the naive controller and also how a controller acts through an estimate of unmeasured coordinates.control.underactuatedhascontroller(compiled, base="naive"|"frozen", correction=None|"passive"|"tank", gravity=), thePassivationoutput stage (the passive and the tank correction: the motors never inject more power than the robot’s own dampers take, or more than the tank holds),Frozen(the frozen configuration, also balancing the robot’s gravity),DirectionalForce(a force along a direction by one scalar of the stiffness, closed form, positive definite),projector,defectandfeasible(the wrenches the motors realize in full). An output stage with aresetis reset with its controller. They agree with the lab’s own code on its three robots (every flag, the frozen point, the force law, and the closed loops).robots.planar:arm("three-link")with one passive joint,arm("five-link")with three, andcontinuum(), a three-section continuum arm driven by two tendons;add_dynamicsandadd_continuum_dynamicsgive them gravity and their springs and dampers.Docs: tutorial “Robots with fewer motors than joints”.
vmc.optimization: plan the motion of a controller on a robot and optimize its Params.Problem(system)is built once and solved many times.freechooses the Params to optimize andparameterthe ones set at every solve (references, say), by name or glob; bounds and scale come from the Param.Collocation(q0, horizon, nodes, initial=, transition=, scales=)is trapezoidal collocation of the closed loop on the robot’s full dynamics, without inverting the mass matrix. With aninitialcontroller, its torques fade out as the new ones fade in with the blend ofSwapController, so the plan is what the swap executes, and the model starts at rest where the robot is whatever it gets wrong there.Effort,Cost(a library coordinate brought to zero from a time on) andBound(a coordinate kept within limits) are the terms,Termthe base to write your own.Resultholds the motion, the Params found, the cost by term and the solver’s status, andapplyputs the values into a running controller (returning the exact energy jump) or into a system.search_referencessearches a reference on a grid, one at a time, keeping the best converged plan.Problem.buildreturns theNLPfor any other solver.Collocation(scheme="hermite-simpson")integrates to fourth order instead of second: it adds the acceleration at the middle of every interval to the unknowns, and needs far fewer nodes for the same agreement with a simulation. The program agrees with the lab’s own optimization code to rounding error, and its solved plans to solver tolerance. Controllers with virtual states and non-flat configuration spaces are not supported yet.optimization.Equilibrium(q0): the closed loop at rest, as a static problem. It takes the place ofCollocation: the unknown is a configuration where the controller’s torques balance the robot’s own forces, found with the free Params, and the terms see it as their one node.CostandEffortcount it once, and aBoundholds there. The tutorial finds where the soft arm rests, in milliseconds.Collocation(periodic=True)plans an orbit that repeats: the last node equals the first (q and v), the first node is free (q0andv0are only the solver’s starting guess), andBoundreaches it.Collocation(free_time=(lower, upper))makes the horizon an unknown within these bounds, starting athorizon: the node spacing, the controller’s time and the coordinates ofvmc.Time()are expressions of it, andEffortandCostintegrate with the horizon found. The windows of the terms still read the nodes’ times at the starting horizon.Result.horizonis the horizon found (the given one for a fixed horizon, 0 for anEquilibrium) andResult.tuses it;warm_starttakes ahorizon.Trajectory.dtis a number or an expression of the free horizon, andTrajectory.timesandTrajectory.horizonare the same, for terms of your own.optimization.Period(param)holds the horizon equal to a Param, so that a controller whose reference is a function ofvmc.Time()and that Param (a period, free or a parameter) repeats with the orbit. Neither option goes with aninitialcontroller. Both schemes work.Docs: tutorial “Optimizing a virtual mechanism”.
Docs: the optimization tutorial finds the period of a mass on a spring, and the least-effort period of a pulled one with a controller that keeps time, against their closed forms.
Docs: the optimization tutorial plans the soft arm around a sphere (
BoundonSphereDistance), checks the clearance in simulation, and shows that two sets of scales give the same plan.vmc.sim.rollout(system, q0, T, dt): the closed loop simulated in one compiled call (mapaccum), differentiable with respect to the controller’s live Params, with the robot integrated by"implicit"(the default, asModelPlant),"rk4"or"cvodes"; it reproducesvmc.sim.runto rounding error.vmc.sim.ode(system)is the closed loop in continuous time, f(t, x) for SciPy’ssolve_ivp.vmc.control.Tank(controller, level, capacity): an energy budget for changing a running controller.tank.set(values)applies a change of live Params as far as the tank can pay for the exact jump it gives the controller’s energy (the largest fraction of the step that fits); a step that releases energy is applied whole, even with an empty tank, and refills it.result.apply(tank)passes an optimizer’s result through it.VMCController.jump(values)gives the jumpsetwould give, without applying it. Run in place of the controller (vmc.sim.run(plant, tank, ...)), the tank also refills itself with what the controller’s own dampers take at every step.identification.fit_stiffness_dampingtakes a run log (RunLog) as it is, or a mapping whose motor torques aremotor_torqueoru.identification.Steps(baseline, pulls, held_out, hold, rest): the step experiment as a controller that runs on any plant. Every motor gets the baseline torque, and each pull adds a torque to some motors forholdseconds, withrestseconds at the baseline between them. Its log marks the training steps (train), which the fit uses as they are; the held-out ones are for validation.fit_stiffness_damping(..., friction=True)adds a static friction torque per motor, F sign(θ̇), to the fit and returns it after K and D (K, D, F). Friction that is not in the fit passes for damping. A motor that stands still has none in the model, so a run that stops at the end of every step hardly shows it: use one that keeps the motors moving.identification.validate(model, run): simulates a robot with its stiffness and damping under the logged torques of the held-out steps of a run, from the state the run is in at the first of them, and returns the root-mean-square error and the variance accounted for of each coordinate.vmc.sim.rollouttakesu, motor torques with a row per step, to simulate open loop.Docs: the hanging arm’s page identifies its stiffness and damping with
Steps, checks them on held-out steps and shows the friction option.identification.fit_transmission(motor, joint): the joint angle per motor angle of a cable drive, from a sweep of the joint, by least squares through the origin. It reproduces the finger’s and the hand’s transmission constants from their recorded sweeps. The finger’s page shows it.vmc.adaptation: laws that change a running controller’s live Params, a step at a time. Everystepreturns the jump of the controller’s energy [J] that was applied, and aTankpassed in place of the controller applies only the part it pays for. A new value stays within the Param’s bounds (a stiffness stops at zero), and a square matrix stays symmetric and positive semidefinite (control.project_psd).ForceTracking(controller, site, params, normal=None, max_force_step=0.01, max_step=None, rate=None)descends the error of a contact force:paramsare glob patterns of live Params (a spring’s goal or its stiffness),normalkeeps the force along it. The gradient is exact, by automatic differentiation of the delivered torque, and agrees with the lab’s hand-derived one to 1e-8 for the goal and the stiffness, at the tip and mid-arm, along a normal and in 3D, also where the point’s Jacobian has rank below 3 (the two-motor finger). A step is the largest that changes the force bymax_force_step[N] (the lab finds it by a finite-difference probe and agrees to 1e-5), orrate, a fixed learning rate as in the finger’s experiments, where it agrees with the lab’s joint-space stiffness and reference descent at the lab’s rates.directiongives the descent direction without applying it.ForceRatio(controller, params, max_change=0.05)andStiffening(controller, params, low, high, alpha): the laws of the lab’s hand and finger that need no model. The first scales stiffnesses by 1 plus or minusmax_changetimes the relative error of the force (the hand’s multiplicative law); the second sets k(F) = low + (high - low)(1 - exp(-alpha F)) from the measured force (the finger’s law). Both equal the lab’s. K(d) is theSigmoidSpringand thePolynomialSpring.StiffnessTracking(controller, site, params, normal=None, robot=None, fraction=1.0): direct stiffness tracking. The stiffness at the site is affine in the springs’ stiffness, sotargetsolves a linear system for the Params that give a wanted matrix, the solution of smallest norm as in the lab’s inversion, andstepgoesfractionof the way. Where no spring pulls it agrees with the lab’s inversion to 1e-6; where they do, it counts the geometric terms that the lab’s leaves out.PositionRegulation(controller, sites, gain=0.05): integral pose regulation. A step addsgaintimes the error of a point of the robot to the goal of the spring that pulls it.HoldingGoals(controller, sites, robot=None): the open-loop counterpart, the lab’s position feedforward. It puts the goals at wanted positions (default: where the points are) plus the smallest offset that lets the springs carry the robot’s own stiffness and weight at the pose now, one Newton step on the static balance. It agrees with the lab’s to 1e-9; with as many springs as motors the arm then stays where it is.
vmc.estimation: quantities of a running robot that no sensor measures. Both take the controller, a site (a name, an arc parameter or(part, s)) and anormal, and arobot: the model that holds the arm at rest, without the surroundings the plant may have. They use the controller’s law before the output stages, the system’s transmission and efficiency, and leave velocities out.ContactForce(controller, site, normal=None, robot=None): the force a robot exerts at a site at rest. What the delivered motor torques and the robot’s own stiffness and weight do not balance goes through the site’s Jacobian by the pseudo-inverse. It agrees with the lab’s contact force from the virtual springs and the structural stiffness (the efficiency on the springs’ term only) to 1e-9, with its torque-based variant, and with the finger’s. A fingertip at rest on a simulated table reads the table’s force to 2 %.TaskStiffness(controller, site, normal=None, robot=None): the stiffness [N/m] at the site, a 3 by 3 matrix. It is the stiffness of the static balance, by automatic differentiation, with the geometric terms (the Hessians of the site and of the springs) counted, and agrees with the lab’s exact apparent stiffness to 1e-8. The lab’s first-order version leaves the geometric terms out and is not provided.KalmanFilter(system, dt, Q=None, P0=None, gate=40.0, robot=None)andMeasurement(q, v, Rq, Rv, observed, name): a Kalman filter of the robot’s state (q, v). Its process model is the robot’s own dynamics, linearized at the estimate at rest and held over the step (the lab’s frozen mass matrix, in general form), with the command going through the system’s transmission. It fuses encoders (kf.encoder) and any sensor that sees part of the state, gates each measurement on its innovation, and reports the sensors it rejected (rejected,rejected_total) and the expected ones that offered nothing (missing). Over a run with dropouts and an outlier of each sensor it agrees with the lab’s filter in its state to 1e-9, its covariance, its innovation distances and the sensors it rejects.Inversion(robot, at, damping, tol, max_iter): the configuration that puts the robot’s pointsatwhere a position sensor saw them (motion-capture markers), by damped Gauss-Newton from the last result, for any robot. It agrees with the lab’s closed form for the three-section arm to 1e-8, and finds the soft arm from its neutral configuration up to 0.6 rad of bend in each section (giveq0beyond).VelocityFilter(dt, alpha=0.3): the low-passed difference of consecutive samples, the lab’s velocity of the markers, withdtthe sensor’s own period.ImuFilter(arm, mounts, kp, acc_tol, gravity): the curvature (Dx, Dy) of each section of a soft arm and its rate from an IMU on its base and on each section end, by a complementary filter of each section’s relative rotation (the gyros turn it, the gravity both accelerometers see corrects it, and it is kept on the rotations the arm can make).calibratetakes the gyro bias and the starting pose from the arm held still. Itsobservedcoordinates go toMeasurement. It agrees with the lab’s filter to 1e-10 over a run with accelerometers that cannot be trusted.
VMCController.inputs(): the vector the law reads, with the live Params as they are now. A Param that acts only through a virtual state does not change the force at once, and the laws leave it.Docs: the tutorials “Track a contact force” (the laws, the estimate without a sensor, the tank) and “Shape the stiffness of the tip”; the finger’s page presses between two force levels; the two arms’ page holds an object with a chosen force, with estimates alone, with a sensor on the object, and through a tank (the object feels the wanted force to 0.2 % from the estimates).
vmc.Gated(component, gate): an element whose force and energy are multiplied by a live Paramgatebetween 0 and 1.optimization.Sparsity(weight, *patterns)adds the sum of the free Params that match (each at least 0) to the cost. With the gates free, the optimizer keeps the elements that earn their place: structure optimization, tested on a mass with three springs and on the soft arm with five repulsive fields, and shown in the optimization tutorial (“Which fields to keep”).vmc.models.FunctionModel(frame, space, params, sites, q_unit): a kinematic model from a functionframe(q, at, p)that returns the rotation and the position of a site, as CasADi values or as lists and arrays. It works inKinematics, mechanisms andcheck_model; a function cannot be written to a file, so it has noto_dict.vmc.testing.check_model(model): the model contract in one call, for a kinematic model or a robot’s mechanism: everything finite (also with each Param a tenth of its default and ten times it), orthonormal rotations, Jacobians, angular Jacobians and Hessians against finite differences, no jump along the arc parameters,to_dictandfrom_dictgiving the model back, and, for a robot with masses and no dampers, an energy drift that falls with the step. It raises anAssertionErrorthat lists the failed checks and returns the worst error of each. The tutorials “Build your own robot” and “Extend the library” use it.vmc.BoxDistance,CapsuleDistanceandCylinderDistance: signed distances from a point to a box with its sides along the axes, a capsule and a cylinder, for contacts and obstacles, with the same Params asSphereDistance(and the kindsbox_distance,capsule_distanceandcylinder_distancein configuration files). The contact tutorial uses the cylinder.vmc.ContactFriction(distance, stiffness, friction): Coulomb friction at a contact, smoothed. It pushes a point against its sliding speed along the surface, with the size of the friction coefficient times the contact spring’s force (give it the spring’s stiffnessParam), and falls linearly below the small speedspeed, so a point at rest creeps. It works on every signed distance, which now share the base classSurfaceDistancewith aunit_normal; the type iscontact_frictionin configuration files. The contact tutorial drags a fingertip along a table.vmc.sim.run(..., record="robot")logs what a simulated robot feels, component by component:robot/<name>/y,ydot,forceandtorqueof each spring, damper and contact, fromModelPlant.elements()(andDynamics.elements). A real robot reports no forces, so recording it raises an error.vmc.estimation.Encoders,Markers,ImusandLoadCell: simulated sensors, the true state in and a noisy reading out, as the estimators take it. The encoders read the motors through the transmission, with noise and a constant slack per motor; the markers, the positions of points of the arm; the IMUs, the angular velocity and gravity in each frame’s own axes, with a constant gyro bias; the load cell, a force with a bias and noise. Every one has a seed. The estimation tutorial uses them in place of the lines it wrote by hand.vmc.adaptation.DitherSeeking(controller, params, amplitude, frequency, gain, window): extremum seeking from the cost alone, the two-tone law of the turtle paper. Each scalar live Param is held at an estimate plus a sinusoidal dither at its own frequency (2π n /window, a whole n each, so that the tones are orthogonal); the cost’s correlation with a tone over the window is the slope with respect to that Param, and the estimate moves downhill atgaintimes it, at mostmax_rate, within the Param’s bounds. On aTank, the changes are paid from its energy, which is the paper’s passivity cap. The crawl tutorial lets the crawler find its bestpeakthis way.turtle.controller(robot, depth=, peak=, steer=, limit=): with any of them the cranks’ springs arePhaseSprings (the paper’s phase-modulated, saturating potential); without, they are the plain ones of the lab’s controller, as before.vmc.optimization.MovingHorizon(system, dt, window=10, Q=, P=): moving-horizon estimation of a robot’s state (q, v). Every step finds the states of the lastwindowsteps that best agree with the sensors’ readings (the sameMeasurements as the Kalman filter’s), with the robot’s own dynamics up to the process noiseQ, and with the state before the window, which goes one step of an extended Kalman filter whenever the window slides. It is solved by Gauss-Newton with a trust region. With a linear robot it is the Kalman filter, to rounding error, however short the window; it also givescostand the covarianceP. The estimation tutorial runs it on the soft arm.vmc.identification.fit_params(robot, names, runs): any Params of a robot, by name or glob, fitted to logged runs by least squares on the residual of its own dynamics, within the Params’ bounds. A residual linear in the Params (masses, stiffnesses, dampings, efficiencies) is solved in a step, and any other, such as the saturating damper’s, by Gauss-Newton. It returns the values in each Param’s shape, their standard errors and the residual’s root mean square. A run with anauses it, and otherwise the acceleration is the smoothed velocity’s difference against the torques averaged over the step they were held. It agrees with ordinary least squares, values and covariance, on a mass, spring and damper. The tutorial “Fit Params to a run” uses it.KalmanFilter(..., unscented=True, substeps=10): the prediction sends sigma points of the estimate (the scaled unscented transform, a root of three standard deviations out) through the robot’s own integrator, insubstepssteps, instead of linearizing at the estimate; the sensors stay linear, so the update is the same. On a double pendulum it follows the mean of a Monte Carlo to 0.003 where the linearized prediction is off by 2; on a linear robot the two agree.vmc.estimation.MomentumObserver(system, dt, gain, robot=None): the external force on a robot from its motion, without accelerations. From the momentum and what the model says would change it, it gives the generalized force the model does not explain, as a low-pass filter of bandwidthgainof the true one, andforce(site, normal)carries it to a site, asContactForcedoes. It works while the robot moves, whereContactForceassumes rest. The force tutorial shows a finger landing on a table. Its velocity must be the rate of its configuration (no floating bodies yet).vmc.estimation.object_compliance(position, force, baseline_position, baseline_force): the compliance in m/N of an object a tip presses, the distance over the force that stiffer settings of the controller add to a gentle one, from the median of each setting’s samples. It is the lab’s probe of the hand, and it agrees with the lab’s code to rounding error. The force tutorial shows it on three objects, with the force fromContactForce.Docs: the contact tutorial also shows two points of a robot touching (a self-contact, the two arms), friction between two points and a soft object between two tips, from the pieces above.
vmc.control.project_psd(K): the symmetric positive semidefinite matrix nearest to a stiffness, for an update that an adaptation law proposes, before it goes throughsetor a tank.vmc.sim.energy_balance(log): the controller’s energy over a run recorded withrecord=["energy"]: its energy, the work given through its port, what its dampers took and its sources gave,injected(what changes of live Params put in, plus the error of the steps) and themarginit can still give, which stays above 0 while the controller is passive.vmc.optimization.Grid,Random,CMAES,ExtremumSeekingandBayes: gradient-free tuning by ask and tell (ask()gives candidates,tell(candidates, costs)takes their costs),tuneto run it, andbounds_offor the bounds and values of named Params.Bayesfits a Gaussian process to the costs seen so far and asks for the candidate with the highest expected improvement: it suits slow episodes, tens of them. The episodes are yours, in simulation or on a robot in your own loop.compile_dynamics(robot, runtime, actuation)takes the actuation of a robot that has none, so a system’s efficiency Params (robot.efficiency.c1) are the ones the dynamics read.vmc.math: rotations and rigid transforms that run on numpy arrays and CasADi symbols alike (rot_x,rot_y,rot_z,rot,skew,vee,exp_so3,log_so3,transform,invert,exp_se3,log_se3,adjoint), for writing theframeof aFunctionModel. Every function is smooth at the identity, also in its second derivatives (the exact forms lose them there). They replace the three rotation helpers ofcore.symbolic.SerialChainjoints:"helical"(a turn that slides by a pitch, given as("helical", pitch)),"spherical"(three coordinates, a rotation vector about the joint’s point) and"free"(six: a translation and a rotation vector, a floating base). A chain on a free base, a brick of point masses thrown with a spin and its momenta and energy are tested; a branch of a tree is a part mounted on a site of another part byAssembly.SerialChainjoint("rail", waypoints): a slide along the natural cubic spline through the waypoints, which are adesignParam (an array of rows of three) in the frame of the joint before it. Its coordinate is the spline parameter, 0 at the first waypoint and 1 at the last. It agrees with SciPy’s natural spline, and a bead on a circular wire swings as a pendulum.SerialChainjoint"floating": a floating base whose rotation is a unit quaternion, with seven coordinates and six velocities (the translation’s and the body’s angular velocity in its own axes).vmc.Quaternionis its space, andSpace.coadjointgives the term that velocities that do not commute add to the dynamics (the Euler-Poincare term), whichcompile_dynamicsnow includes. A body tumbles about its intermediate axis through several full turns by Euler’s equations, and a base with an arm keeps its momenta and energy.vmc.FramePoint(model, site, q=coordinate): a point of a virtual model, at the configuration thatcoordinategives: a virtual state (a cart on a rail with a mass of its own), a reference, or a function of time.vmc.Time()is the time of the run, to build functions of time withCustom; a damper on the difference to such a goal feels its velocity.More coordinates:
vmc.Sum(and the plus sign:tip + [0, 0, 0.05]),vmc.FrameRotation(a frame’s rotation matrix, row by row),vmc.OrientationError(model, site, goal=)(the rotation vector from a goal orientation to a frame’s, in the goal’s axes, withgoala live Param; a spring on it is a rotational spring whose torque is exact at any error, where the one from the angular Jacobian is 8 % off at 0.4 rad),vmc.InFrameandvmc.FromFrame(a vector along the axes of a frame, and back: a stiffness along the tool’s axes turns with the tool). They take a site or an arc parameter, and a virtual model throughq=. Configuration files build them assum,rotation,orientation_error,in_frameandfrom_frame.More components:
vmc.RotationalInertia(rotation, inertia), the inertia of a rigid body about its frame (a matrix, or three principal moments) on the frame’s rotation matrix, next to aPointMassfor its mass: it follows Euler’s equations, moves like the same body made of point masses to rounding error, and a rotational spring on it oscillates at the square root of K over I.vmc.DiodeDamper(y, D, sign, smoothing)damps one direction of motion only.ForceSource(y, f, max_force=, max_power=)bounds the force smoothly, and the power it delivers.Mechanism.add_mass_along(name, mass, s0, s1, n)spreads a mass along a model as point masses at the nodes of Gauss-Legendre quadrature. AnInertanceon a difference is an inerter, tested.Docs: “A spring on the tool’s orientation” and “A spring along the tool’s axes” in “Kinematics on the UR5”; “A damper that works one way” in “Coordinates and components”; a rigid body and the mass along an arm in “Build your own robot”.
Docs: the tutorial “Estimate the state of a soft arm” (encoders with slack, markers and IMUs fused by the filter, an outlier and lost frames); “A rail along a path” and the floating joint in “Build your own robot”; “A cart on a rail” in “Coordinates and components”.
vmc.PhaseSpring(coord, stiffness, depth, peak, steer, side, limit): a spring on a deflection e whose stiffness follows a phase, on the coordinateStack(e, phase): K (1 + side steer) (1 + depth cos(phase - peak)). Its force is minus the gradient of its energy, so the phase feels the reaction that keeps a flywheel loop passive. Withlimitthe potential saturates, and the torque never goes above K (1 + |steer|) times the limit. With no depth, no steer and no limit it is aLinearSpring. The type isphase_springin configuration files.vmc.models.Passive(nv): an actuation with no motors, for a part that nothing drives, such as a floating body. Stacked with the other parts’ motors (Assembly.stacked_actuation), the motor vector holds only theirs, so a simulated robot can have more coordinates than motors. The motors do not give a passive part’s configuration, so a controller is compiled against a robot without it, and the simulator runs the one with it.robots.turtle.crawler(**CRAWLER): a simple crawler for the simulator, with placeholder constants, not the lab’s turtle: a floating body on the ground with gravity, two cranks about its lateral axis with a foot each, and a contact spring, damper and friction at each foot and at the four corners of the underside. Its motors are the two cranks, in the gait convention ofturtle.robot(), which is what the controller is compiled against. Under the flywheel controller it crawls forward, further the faster the flywheel is driven, and does not advance without friction; a positive steer ofPhaseSpringturns it left. Docs: the tutorial “Crawl with a flywheel”.
Removed¶
vmc.ros(RosPlant,control,serve,LiveParams,JointIO,TOPICS) and the direct Dynamixel path (hardware.DynamixelPlant,home,present_ticks,scan,latency_timer,Bus,SdkBus,FakeBus), deprecated in 0.3.0. The library is communication-agnostic: it turns measurements into torques, and how they reach the robot stays outside it. The Dynamixel SDK and pyserial are no longer dependencies, and CI no longer runs the ROS tests. Hardware profiles (HardwareProfile,Motor,KT,MODESand each robot’shardwaretemplate) stay. A project that still uses a removed name keepsvirtualmodelcontrol==0.3.0.vmc.interactive(the window, the keyboard, the joystick, the session, the mailbox, the wrapper and the recorder), thejoystickextra andSimClock’sspeed,nowandsleep: the library is for control, not for interfaces. A running controller still changes throughcontroller.set, schedules and swaps. A project that uses them keepsvirtualmodelcontrol==0.3.0.From the hardware profiles, the raw Dynamixel units (encoder ticks, velocity units, goal currents, motor constants
ktand theKTtable, baud rate, serial port, velocity filter, home ticks, per-motor torque limits and operating modes). AMotoris nowMotor(id, sign, hold)and aHardwareProfile(motors, rate, bus_order), with the conversions between a driver’s degrees, bus order and signs and the library’s SI units.
0.3.0 - 2026-10-05¶
Added¶
vmc.config: experiments in YAML files.vmc.config.loadreads the robot (a template and its arguments), named coordinates, the controller’s elements (a registered componenttype, thecoordinateit acts on, its gains), controllers to swap to, and how to run them: a simulation (with the robot’s dynamics and simulated elements such as contacts), the rate, the duration, output stages, schedules and the initial virtual state.Experiment.runruns it,savewrites it back with the Params’ current values. Robot templates, output stages and coordinate kinds are registry entries, so a project adds its own. An experiment from a file gives the same run as the same experiment written in Python.vmc.control.ScheduleandScheduledController: live Params that follow values over time (in straight lines or steps) and swaps at given times; a new run starts afresh. The lab’s linear ramps are two-point schedules.vmc.sim.run(..., z0=...)also takes a function of the first reading, such asturtle.initial_state.Docs: tutorial “Experiments in files”.
Run logs.
vmc.sim.RunLogsaves a run whole or not at all (save: a compressed.npzwith a schema version and what the run was, inmeta: the library’s version, the start time, the Params at the start, the plant’s hardware profile and, from a configuration, the configuration; written through a temporary file, and a name already taken is refused unlessoverwrite=True), loads it back (load) and exports CSV (to_csv, one column per entry).stepadds one step and keeps every signal at one row per step, with NaN for what a step did not compute.vmc.sim.replaysends a log’s motor torques again to a simulated robot, each held for as long as it was in the run, from the log’sqandvor, for a real robot’s log, from its first motor reading (ModelPlant.state_from_motors);vmc.sim.comparegives the largest and the root-mean-square difference of each signal of two runs. A replay on the true model gives the run back.A run’s log holds
law_torque, the controller’s torque before the output stages.vmc.sim.run(..., record=...)adds the live Params ("params"), each element’s coordinate, rate, force and share of the motor torques ("elements"), and the controller’s energies and powers ("energy"), fromVMCController.elements()andbalance().Configurations:
experiment.runsettings (name,folder,overwrite,record) save each run beside the file and refuse a name already taken before the run starts.Docs: tutorial “Run logs”.
vmc.interactive: change a running controller by hand.Controlsis a thread-safe mailbox for new values of live Params and for swaps.Interactivewraps any controller and applies what the mailbox holds just before each step, in a simulation or inside a robot’s own node; the changes act exactly ascontroller.setbefore the step, and it counts the energy they give the running controller (injected).Recorderturns what was applied intoscheduleentries withstepinterpolation and saves them into a configuration, which repeats the session exactly.Window(matplotlib, no new dependency) draws the robot live, with goals to drag, sliders, buttons to swap controllers, record and pause;KeyboardandJoystick(pygame, the extravirtualmodelcontrol[joystick]) move a goal;Sessionruns a simulation in real time in a worker thread with its window.vmc.sim.SimClock(dt, speed=...)paces a simulation to the computer’s clock: withspeed,run(T=None)runs until stopped and Ctrl-C ends it with the log so far. Other simulations are unchanged.VMCController.live_params()andExperiment.z0().Docs: tutorial “Interactive control”.
Docs: tutorials “Real-time runs” (the loop on the computer’s clock: measured steps, late steps never caught up in a burst, old and missing readings, Ctrl-C, and a checklist for the first run on a robot) and “Swaps and schedules” (the quintic blend and its continuity, a swap on the soft arm, the schedules’ interpolation).
vmc.ros.control: the controller node. It runs a controller at the profile’s rate on the robot behind the driver’s topics, with its live Params as ROS parameters, for a duration or until Ctrl-C, and ends with zero torque.On real time (
WallClock),vmc.sim.run(..., T=None)runs until Ctrl-C, and Ctrl-C ends a real-time run with the log so far.CI runs the ROS tests in ROS 2 Humble and Jazzy containers, on a private domain.
vmc.control.SwapControllerandblend_weight: one set of virtual elements replaces another with a quintic blend of the two controllers’ torques (continuous torque and first two derivatives), the blend the lab’s controller uses; a swap asked for during a blend waits for it.vmc.ros.control(..., swaps={name: controller})swaps on aStringmessage.vmc.Efficiency: the motor torque a transmission delivers, as a polynomial of the commanded one, τ = c₁u + c₂u² + … + cₙuⁿ, each coefficient one value for every motor or one per motor. The coefficients aredesignParams, to change, tune, keep live in a simulator or identify.DirectandTendonTransmissiontake one, or a number for its linear coefficient.vmc.identification:plateaus, the settled end of every hold of a command in a log, andfit_efficiency, a least-squares fit through the origin, motor by motor or shared, from delivered torques or from one measured quantity with a weight per motor (a fingertip force, say). Both agree with the lab’s calibration code, and the fit gives the finger’sadapt.MOTOR_EFFICIENCYback from its recorded data.Docs: concepts pages “Transmission efficiency”, “Virtual Model Control” (coordinates, forces, the torques that realize them, virtual states), “Passivity” (the energy balances, stability, what weakens it) and “Finger and hand kinematics” (motors to joints, joints to the fingertip, the hand’s couplings).
The constrained elements as components of their own:
ConstrainedLinearSpring,ConstrainedTanhSpring,ConstrainedGaussianSpring,ConstrainedLinearDamperandConstrainedTanhDamperact along one direction (normal) only, leaving the plane normal to it free. They give the forces of the lab’s constrained elements, checked against them; the tanh ones saturate along the normal rather than axis by axis.vmc.identification.fit_stiffness_damping: a robot’s diagonal stiffness and damping from logged runs under known motor torques, by least squares with K, D ≥ 0 in motor torques, relative to a resting baseline, with the robot’s masses and gravity known; each run can carry its own gravity. It recovers the stiffness and damping of a simulated arm within a few percent.Docs: two single-arm examples, each about its own arm and its own experiments. The soft arm on its side (
145-145-145) reaches past an obstacle with two repulsive fields, over a sweep of their strength, and a tanh spring limits the force on a string tied to the tip. The hanging arm (145-290-290) is identified from simulated step responses withfit_stiffness_dampingand reaches around an obstacle with a tanh spring and two fields. Both use the same stiffness and damping. The tutorials, the home page and the README use the arm on its side; the Helyx geometries and the parametric template are on the soft-arm kinematics page.
Changed¶
Every robot’s efficiency is 1 by default: models identified from the commanded torques already include their transmission. The soft arm and the two arms take each torque as the controller sends it again, with
helyx.SIM_STIFFNESS,helyx.SIM_DAMPING,bimanual.STIFFNESSandbimanual.DAMPINGreferred to the commanded torque (the identified values divided by the 0.12 they were fitted with), as in 0.1.0. 0.2.0 applied 0.12 to every torque with physical stiffness and damping: the same equilibria without gravity, but natural frequencies about 2.9 times lower and a gravity compensation that cancelled only 12 % of the weight.helyx.EFFICIENCY,bimanual.EFFICIENCY,adapt.MOTOR_EFFICIENCYandadapt.HAND_MOTOR_EFFICIENCYstay as calibrations, never as defaults.A transmission’s efficiency Param is now
efficiency.c1(andc2, … for a polynomial); transmissions saved by 0.2.0 still load.import virtualmodelcontrolno longer loads SciPy, which took about three quarters of its import time;identification.fit_stiffness_dampingloads it when called.Docs: the opening page has no left index (the pages of the five sections keep theirs) and leads to the concepts and the reference. Installation: the conda recipe takes Python from conda-forge, because conda’s default channels stop a new installation until Anaconda’s terms of service are accepted, and shows how to keep ROS’s
PYTHONPATHout of a conda environment, where pip otherwise ends with an error about ROS’s packages and skips the ones ROS also ships.
Deprecated¶
vmc.ros(RosPlant,control,serve,LiveParams,JointIO,TOPICS) and the direct Dynamixel path (hardware.DynamixelPlant,home,present_ticks,scan,latency_timer,Bus,SdkBus,FakeBus) warn when used, and leave in 0.4.0 together with the Dynamixel SDK and pyserial dependencies and the ROS CI job. The library is communication-agnostic: it turns measurements into torques, and how they reach the robot stays outside it. Hardware profiles (HardwareProfile,Motor,KT,MODESand each robot’shardwaretemplate) stay. Python shows aDeprecationWarningonly for code run as a script or in a notebook; run a node withpython -W defaultto see them there.
Fixed¶
vmc.sim.runneeds only the plant’s documented methods: when its guard trips, it sends zero torque without reading the plant’s last commandu, which a plant of one’s own may not have. The zero command has one torque per motor rate, also on real time.vmc.rosis available afterimport virtualmodelcontrol as vmc, likevmc.vizandvmc.hardware; it imports ROS only when a ROS class is first used.The hand example grasps the ball with the pads of the thumb, index and middle fingers: the ball sits where the thumb opposes the fingers, and the grasp takes the thumb’s tip at the end of its last phalanx. Before, the thumb pressed on the ball with its side. The template’s kinematics are unchanged.
Figure labels written as math where the figure font has no glyph (N·m on the efficiency page); a test keeps figure text to characters the font draws.
0.2.0 - 2026-10-04¶
Added¶
Every dependency installs with the package: numpy, SciPy, CasADi, matplotlib, PyYAML, the Dynamixel SDK and pyserial. CI runs on Ubuntu 22.04 and 24.04, with a clean-install job.
Robot templates:
robots.bimanual(two Helyx arms on one frame, with their identified stiffness and damping and the transmission efficiency 0.12),robots.adapt.hand(five digits, 13 motors, spread coupling, masses and joint limits),robots.turtle(two cranks and the virtual-flywheel controller),robots.ur5(from DH parameters) andur5.with_hand().Every template is parametric in its geometry, with today’s values as defaults:
helyx.arm(lengths=..., section_radius=..., spool_radius=..., tendon_angles=..., masses=...)for any number of segments;bimanual.arms(lengths=..., tendon_angles=..., base_positions=..., efficiency=...), each one for both arms or by arm;adapt.finger(link_lengths=..., link_masses=..., link_cogs=..., joint_axes=..., motor_radius=..., pulley_radius=..., pip_transmission=...)andadapt.finger_coupling;adapt.hand(...)andur5.with_hand(...), which replace any entry of the hand’s geometry tables by key (tip_offsets={"thumb": ...}), with the hand’s mounting position on the flange;ur5.arm(d=..., a=..., alpha=...); joint-limit springs with custom ranges; output stages andadd_dynamicssized to the arm.vmc.Kinematics: positions, rotations, Jacobians, angular Jacobians and Hessians of any site, from configuration or from motor angles.models.JointSpacefor robots described by their joints only;SerialChain.from_dhand site rotations;Assemblyparts mounted on another part’s frame.TendonTransmission(efficiency=...)andDirect(efficiency=...)(one value or one per motor): the robot receives the commanded torque times the efficiency.adapt.finger,adapt.handandur5.with_handtakeefficiency=...and stay lossless by default: the measuredadapt.MOTOR_EFFICIENCYandadapt.HAND_MOTOR_EFFICIENCYare static calibrations for force estimates, and unequal efficiencies on coupled motors make virtual elements non-conservative in simulation.SpeedRegulator: drives a virtual state at a commanded speed, with a ramp.control.output, opt-in output stages for real hardware:FrictionCompensation,Pretension,TorqueOffset,TorqueLimit; each template’s own stage (helyx.output_stage(),bimanual.output_stage(),adapt.output_stage(),adapt.hand_output_stage()).vmc.viz: the lab figure style (use_style,saveas PDF and SVG) and drawing helpers (draw_robot,draw_spring,draw_damper,draw_frame,draw_force,draw_goal,label_axes).vmc.vizis available afterimport virtualmodelcontrol as vmcand loads matplotlib only when first used.vmc.viz.animate: an animation of a run (the robot, springs to fixed points, the path of a site, anything else through a callback), saved as MP4, GIF or animated WebP.imageio-ffmpegis now a dependency, so MP4 works on every system.vmc.sim.run(..., z0=...)starts the controller from a given virtual state, and the run log records the virtual statezwhen the controller has one.Contact as stiff one-sided springs: signed distances
PlaneDistanceandSphereDistance,ContactSpring(optional smoothing for optimization) andContactDamper(damps only in contact);adapt.add_dynamicsgives the finger and the hand their gravity for simulation.Documentation rebuilt for students: the Material layout (sections as tabs, the pages of each section in the left sidebar), large type, the library’s logo and navy colours; installation and troubleshooting; tutorials (how it works, a first controller, coordinates and components, parameters, energy, kinematics, tuning, contact, building a robot, extending the library); an example per robot (soft arm, two arms, finger, hand and the UR5, turtle), each opening with its robot’s schematic and parameters; concepts (the library’s structure, soft-arm kinematics, conventions and glossary). Every figure, schematic, table value and animation is computed when the documentation builds.
vmc.hardware.HardwareProfile: the motors of a robot (bus ID, Dynamixel model, sign, mode, motor constant, optional torque limit) in the library’s motor order, with the bus settings; conversions both ways between encoder ticks, raw velocities, goal currents and the published degrees of a ROS driver and SI units, with commands clamped before the integer conversion so they saturate instead of overflowing; saved and loaded as YAML. Each template has its profile:helyx.hardware(),bimanual.hardware(),adapt.finger_hardware(),adapt.hand_hardware(),turtle.hardware().vmc.hardware.DynamixelPlant: a robot’s Dynamixel motors as a plant through the Dynamixel SDK, without ROS. It sets a safe goal before each motor’s torque goes on, takes the start positions as zero, reads angles and smoothed rates, writes torques as clamped goal currents, sends zero torque when commands stop (a watchdog), and on leaving (exceptions and Ctrl-C included) sends zero torque and switches the torque motors off; held motors keep their position.hardware.homedrives the motors slowly to recorded home positions and refuses a motor that lost a turn;hardware.scanfinds the motors and their baud rate;hardware.FakeBusstands in for the bus in tests and dry runs.vmc.sim.WallClock:vmc.sim.runon real time, the loop of simulation unchanged. Each step runs on the computer’s clock with its measured time; a reading older thanstalesends zero torque; a late step starts the next one from now instead of catching up; the log records each step’sdt, andlog.infothe rate, the slowest step, the overruns and the stale readings, with a warning when steps overran.virtualmodelcontrol.ros(ROS 2, any distribution; rclpy and std_msgs, loaded only when used):RosPlant, the robot behind the lab driver’s topics (degrees, the bus order, raw signs, torques kept within the range of the driver’s goal current);serve, a digital twin that publishes a simulated robot on the same topics, with a lockstep mode in which a run over ROS repeats the simulator’s;LiveParams, a controller’s live Params as ROS parameters, applied between steps.
Changed¶
The soft arm models the efficiency of its tendons: it receives η = 0.12 times the commanded torque (
helyx.EFFICIENCY), andhelyx.SIM_STIFFNESSandhelyx.SIM_DAMPINGare the identified, physical values. 0.1.0 divided them by 0.12 and delivered the full torque: the same equilibria without gravity, but natural frequencies about 2.9 times too high and a sag under gravity about 8 times too small. Controllers are unchanged and never divide their torques by η, so the simulated arm receives η of every command, gravity compensation included.The figure style without LaTeX uses matplotlib’s own Computer Modern font (
cmr10) instead of DejaVu Serif, with tick labels set as math. Figures made with LaTeX are unchanged.
Fixed¶
RunLogcopies each row, so a signal updated in place (a controller’s virtual state) is recorded correctly at every step.bimanual.add_dynamicstakes the stiffness and damping of one arm (a dict by arm, arrays included) and keeps the other arm’s identified values.viz.draw_robot: no joint markers along continuum arms mounted in an assembly; robots without geometry (joint-space models) draw nothing instead of failing; serial chains end at theirtoolsite (the UR5 flange) as well as at atip.
0.1.0 - 2026-10-04¶
First public release.
Added¶
Package scaffold:
pyproject.toml(hatchling + hatch-vcs), tests, docs, CI workflows.core:ParamandParamSetwith scopes (fixed, design, episode, stage) and aBindingthat keeps live Params symbolic and folds the rest in; spacesEuclidean,SO2,Product; symbolic helpers;Signals; a registry with plugin entry points; unit strings.mechanisms: coordinates (FramePoint,Joint,State,Ref,Difference,Slice,Stack,Projection,Norm,Custom), components (linear, tanh, Gaussian, sigmoid, polynomial and limit springs; linear and tanh dampers;PointMass,Inertance;ForceSource,GravityCompensation) andMechanism. A tanh spring on a projection saturates at its maximum force in every direction.models: parametricPCCwith n segments,TendonTransmission(θ > 0 pulls),Directdrive, andSerialChain(product of exponentials).robots.helyx: the three Helyx arm geometries with their default parameters.VirtualMechanismSystem,compile()andVMCController: about 20 µs per control step for three springs and three dampers on a three-segment arm (benchmarks/tick.py).compile_dynamics(): robot dynamics assembled from the robot’s components (canonical residual, mass matrix, energy, power, linearly implicit Euler step);Gravitycomponent.sim:PlantandSimPlantprotocols,ModelPlant,run()with aGuard(zero torque on missing or non-finite readings) and aRunLog.models.Actuationprotocol;helyx.add_dynamicswith the simulated arm’s stiffness and damping.Docs: closed-loop simulation tutorial and an “Extend the library” how-to.
models.LinearCoupling(one motor driving several joints),models.Assembly(several models mounted on one base) withStackedActuation;FramePointaccepts a part and an arc parameter.robots.adapt: the ADAPT finger (two motors, coupled distal joints), joint-angle coordinate and joint-limit spring.