# 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](tutorial_a.md), so read that first. The definitions of the new ideas are in [the multibody note](../multibody.md): 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`:* ```python 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](../multibody.md) 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`:* ```python 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`:* ```python 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](install.md#precision-is-per-rig)). ## 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 `DescriptionWarning`s 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 ``, 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`:* ```python 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") ``` ```text 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](../results.md) says how to read a run's outputs beyond the distance printed here.