Tutorial A0: a magnet on an arm, in a closed loop#
Experiment A0 replaces A’s rotating field with a permanent magnet carried by a robot arm. The arm is an AR4. A motor on its last link turns a rotor that carries de Jongh’s N45 magnet, and the magnet is a point dipole above the same screw in the same tube. The screw is no longer given a field: it is pulled and turned by the magnet’s, and a controller moves the arm every step to keep the magnet above the screw as it swims. That is the closed loop.
The file is examples/tutorial_a0.py. It reuses the screw and the declared values of
tutorial A, so read that first. The definitions of the new ideas are in the multibody
note: a chain, its segments, and what a part mounted on a frame means.
What this tutorial claims#
That the experiment builds and runs as written, and that its output is the output shown. The run is short, one second of the screw’s motion, so it says nothing of the screw’s steady swim.
What the test suite holds about A0 is narrow too. tests/differential/test_a0.py holds it to the earlier line’s A0, frozen,
with every difference declared before the comparison ran, in float32. It checks the framework against its reference on the
same drag weights; those weights inherit the old wall tables’ defect (MIME-ANO-117), so it does not check the drag’s
physics, and the earlier code is the weakest oracle.
The apparatus: an arm, a motor and a magnet#
The arm and the motor are two segments of one chain. The magnet is a part mounted on the motor’s rotor, and the field it makes is a point dipole:
From examples/tutorial_a0.py:
def rotor_magnet_on_an_arm() -> Apparatus:
"""The AR4 carrying a motor on its last link; the rotor carries de Jongh's 35 mm by 20 mm magnet."""
return Apparatus(
"rpm",
chain={"arm": Arm.Kinematic(description=description(AR4), initial_positions=rad(HOME), at=m(BASE)),
"motor": Motor.Kinematic(axis=Z, prescribed="velocity", initial_angle=rad(0.0), mount="link_6")},
parts={"rotor_magnet": Part(Cylinder(radius=mm(17.5), length=mm(20.0), axis=(1.0, 0.0, 0.0)),
mount="motor.rotor")},
magnets={"rotor_magnet": PermanentMagnet(direction=(1.0, 0.0, 0.0))},
fields={"magnetic": PointDipole(part="rotor_magnet")},
# The arm's joint positions are given no drive: the controller writes them.
commands={"motor.velocity": Constant(rad_per_s(2 * math.pi * FREQUENCY.value))})
The arm is Arm.Kinematic: a kinematic segment prescribes its motion (the multibody note defines the
word), so it moves where it is told. Its joint positions have no drive here, because the controller writes them.
The experiment#
The tube, water and the screw are A’s. The magnet’s moment is declared with its reason, and the laws are A’s with the magnet’s force added: the magnet’s field is not uniform, so it pulls the screw as well as turning it:
From examples/tutorial_a0.py:
def a0() -> Experiment:
experiment = Experiment("tutorial_a0", scene=Scene.new(up=Z, units="m"), gravity=m_per_s2(9.81), dt=ms(0.5),
duration=SHORT)
tube = experiment.add_structure(Structure("tube", geometry=Tube(axis=+X, inner_radius=mm(3.175), wall=mm(1.6),
length=mm(100.0))))
water = experiment.add_medium(Medium("water", fills=tube.lumen))
experiment.add_apparatus(rotor_magnet_on_an_arm())
experiment.declare("rpm.magnetic_moment", A_m2(18.89), reason="de Jongh's 35 x 20 mm N45 rotor magnet")
umr = experiment.add_body(Body("umr", dynamics=RigidInertial(), initial=State(
position=mm((0.0, 0.0, -1.0)), orientation=(1.0, 0.0, 0.0, 0.0), velocity=m_per_s((0.0, 0.0, 0.0)),
angular_velocity=rad_per_s((0.0, 0.0, 0.0))), parts={"shell": Part(FL9)}))
for path, value in (("umr.mass", kg(8.491116020725203e-05)),
("umr.inertia", kg_m2((1.0331989974018431e-10,) * 3)),
("umr.buoyant_volume", m3(6.022068099805108e-08)),
("umr.magnetic_moment", A_m2((0.0, 2 * 8.4e-4, 0.0))),
("umr.centre_of_buoyancy", m((0.0, 0.0, 0.0))), ("umr.magnet_centre", m((0.0, 0.0, 0.0))),
("water.density", kg_per_m3(1000.0)), ("water.viscosity", mPa_s(1.0))):
experiment.declare(path, value, reason=REFERENCE)
experiment.attach(Drag.Surrogate(weights=asset("dejongh_mlp_v2"), lubrication=Lubrication(epsilon=mm(0.15))),
body=umr, medium=water)
experiment.attach(Magnetic.Torque(), body=umr, field="rpm.magnetic")
experiment.attach(Magnetic.Force(), body=umr, field="rpm.magnetic")
experiment.attach(Gravity(), body=umr, medium=water)
experiment.attach(CentreContact.Projection(margin=mm(0.1)), body=umr, surface=tube.inner_surface)
experiment.observe(SwimSpeed(umr, along=tube.axis))
return experiment
Closing the loop#
The controller reads the screw’s pose and twist and the arm’s joint positions, and writes the arm’s joint positions. It
reads things a real rig could not: it reads the physics state directly, the ground truth (the build report labels each read
state, ground truth, not deployable), which is why it is a controller for simulating the loop and not one to deploy.
From examples/tutorial_a0.py:
def close_the_loop(experiment: Experiment) -> Experiment:
"""The controller reads the body's pose and twist and the arm's joints, and writes the arm's joint positions
every step."""
tracking = ArmTracking(description=description(AR4), end_effector="link_6", home=rad(HOME), axis=X, up=Z, at=m(BASE))
experiment.set_controller(
tracking, reads={"body_pose": "umr.pose", "body_twist": "umr.twist", "joint_positions": "rpm.arm.joint_positions"},
writes={"joint_positions": "rpm.arm.joint_positions"}, mode="graph", rate=ms(0.5))
return experiment
mode="graph" puts the controller inside the compiled step. MIME has other places to put a controller, with different costs;
the build report says which it chose and what that means for when it reads and when its writes act. The controller computes in
single precision whatever the run’s precision, so a run in double precision with it is mixed (the precision
rule).
Run it#
Run it from a source checkout of MIME, in an environment where MIME is installed: python examples/tutorial_a0.py.
examples/ is not part of the installed package, and the arm’s description is the AR4’s URDF in the checkout’s
examples/descriptions/ar4/, under the MIT licence beside its meshes’ manifest (a lab uses its own). The regions below use names the file defines at its top
or takes from script_a.py: AR4 (that path), BASE and HOME (where the arm stands and its joint positions),
FREQUENCY (the field’s rate) and the screw, the reason string and the contact (FL9, REFERENCE, CentreContact).
Running it prints DescriptionWarnings on stderr, apart from the output below, each more than once: about the AR4’s file, the visual
and collision elements MIME does not simulate, and links with geometry and no <inertial>, which it reads as massless.
They are about the description and not about the experiment: the arm is kinematic, so it moves where the controller writes
and not under forces.
From examples/tutorial_a0.py:
built = close_the_loop(a0()).build()
print(built.report.controller[0].split(" (")[0])
history = built.run(SHORT, history=True).history
# The rotor's position is the motor's second frame; the body's is its pose's first three numbers.
magnet = np.asarray(history["rpm.motor"]["frame_poses"])[:, 1, :3]
body = np.asarray(history["umr"]["pose"])[:, :3]
distance = np.linalg.norm(magnet - body, axis=1)
print(f"after a short run of {SHORT.value:g} s, the magnet is {distance[-1] * 1e3:.1f} mm from the body")
controller: arm_tracking 0.1.0, mode 1
after a short run of 1 s, the magnet is 151.0 mm from the body
The first line is the build report’s line for the controller. The second is the distance from the magnet to the screw at the
end of the short run. The controller holds the magnet at its standoff setting above the line of the tube’s axis,
at the screw’s place along it. The printed distance is that
standoff, plus the screw’s centre lying about a millimetre below the axis (it rests on the tube’s floor, and starts there),
plus a tracking error of a fraction of a millimetre along the tube and across it. (tests/docs/test_tutorials.py holds the
excess over the standoff to the screw’s starting depth.)
tests/differential/test_a0.py holds the swim speed, the rotation rates and the axial tracking error, among other
quantities, to the earlier line’s, in float32, at two frequencies. The distance printed here is not one of them: the test
reports it and does not gate it, because it cannot tell the two frequencies apart and so tells nothing the others do not.
This run only shows that the loop is closed and the arm followed.
Where next#
The tutorial for experiment G waits for a1. The results note says how to read a run’s outputs beyond the distance printed here.