# Tutorial A: a screw in a tube of water Script A is de Jongh's FL-9 screw resting on the floor of a quarter-inch tube of water. A uniform field turns about the tube's axis, the screw turns with it, and it swims along the tube. You build it twice: first as the **reproduction arm**, which declares every value the earlier line's run of A used, each with where it came from; then as the **record arm**, which takes water's density and viscosity, and the magnets' moment, from records instead. Both arms are in one file, `examples/script_a.py`, and the tutorial's own run of them is `examples/tutorial_a.py`. The definitions of what the code uses are not repeated here: [the scene](../scene.md) (frames, regions, the stage), [materials](../materials.md) (how a value reaches an entry, declared against fitted), and [the profiles](../profiles.md). ## What this tutorial claims It claims that the experiment builds and runs as written, and that its output is the output shown. It claims nothing about the swim speed it prints, because the run is a short one: the earlier line's reference reads the second half of a longer run, and this run is shorter on purpose, to fit a test's time. What the test suite holds about A is separate, and narrow. `tests/differential/test_script_a.py` holds the reproduction arm to the frozen outputs of the earlier code with every difference declared before the comparison ran: it bounds *how far MIME 0.3's A is from the earlier line's A* at this operating point. The earlier code is the weakest oracle there is, so the agreement says nothing about the flow or about hardware. ## Before you run it **Assets.** Tutorial A fetches nothing: the drag surrogate's weights and the two material records ship with MIME. A tube experiment whose drag is the MRS rung is different. It reads the table of the tube's wall, which is not shipped, and the build is refused by name until it is in the cache: fetch it once with `mime assets fetch unit_tube_R0.997` ([install and environment](install.md#assets-a-run-fetches) says more). **The time step.** A's base step is the one the earlier line's reference ran with, half a millisecond; it is kept so that the comparison to that reference is of like with like, and nothing in this guide shows it to be the largest step that serves. `docs/choosing_spacing.md` measures what the step does in a different experiment (the MRS screw in a tube), and its numbers are not A's. What MIME states about a step, it states in the build report's `limits` and `validity` lines ([reading it](build_report.md#limits)). For the rigid body there is one: the gyroscopic term is resolved only while the rotation rate times the step, `|omega| dt`, is below the number the line prints (MIME-ANO-129). A's field turns at ten hertz, so its `|omega| dt` is well below it; and A declares the same inertia about every axis, for which the gyroscopic term is zero. Those lines are declared, and nothing checks a step against them: the build does not refuse a step past a limit, and a step inside the limits that are stated is not thereby shown to run. A run that ends with a state that is not finite is `run.non-finite`. It names the first step, thing and field only where the run kept every step's state (`history=True`); with only the series it recorded it names which of them went non-finite, and with neither it says the first step is not known. This tutorial's runs record one series and keep no history ([reading an error](../reading_errors.md)). If your own step does that, shorten it, and read the `limits` lines of the report before suspecting anything else. ## The world, the body and the field The reproduction arm starts from the world and the tube: *From `examples/script_a.py`:* ```python # The world: up is +z, so gravity points along -z. experiment = Experiment("script_a", scene=Scene.new(up=Z, units="m"), gravity=m_per_s2(9.81), dt=dt) # The 1/4" silicone tube, along +x, and the water inside it. 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.declare("water.density", kg_per_m3(1000.0), reason=REFERENCE) experiment.declare("water.viscosity", mPa_s(1.0), reason=REFERENCE) ``` The tube is `Tube(...)` of the scene, its `lumen` is the region water fills, and the two `declare` lines give water's density and viscosity as numbers with a reason ([materials](../materials.md) says what a declared value is). The body is the screw and its declared mass, inertia, buoyant volume and magnetic moment, each with its reason: *From `examples/script_a.py`:* ```python # FL-9, resting on the tube's floor 1 mm below the axis, its own axis along the tube's. Its shell is the de Jongh # helix (de Jongh et al. 2025, Eq. 1 and Table 1), which gives the drag its shape and the body its extent. 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)})) experiment.declare("umr.mass", kg(8.491116020725203e-05), reason=f"{REFERENCE}: its m_eff, (410 + 1000) kg/m^3 times FL-9's volume, with no added mass") experiment.declare("umr.inertia", kg_m2((1.0331989974018431e-10,) * 3), reason=f"{REFERENCE}: its I_eff, m R^2 / 2 for a solid cylinder of FL-9's radius, used about " f"every axis alike") experiment.declare("umr.buoyant_volume", m3(6.022068099805108e-08), reason=f"{REFERENCE}: FL-9's volume") experiment.declare("umr.centre_of_buoyancy", m((0.0, 0.0, 0.0)), reason="the reference's body has one density, so its centre of buoyancy is its centre of mass") experiment.declare("umr.magnetic_moment", A_m2((0.0, 2 * 8.4e-4, 0.0)), reason=f"{REFERENCE}: two magnets of 8.4e-4 A m^2, across the body's axis") ``` The field is an apparatus with a field provider, turning about the tube's axis, and its two commands are the frequency and the amplitude: *From `examples/script_a.py`:* ```python # The field, starting along the body's moment and turning right-handed about the tube's axis. field = experiment.add_apparatus(Apparatus("field", fields={ "magnetic": UniformRotating(rotation_axis=tube.axis, initial_direction=(0.0, 1.0, 0.0))}, commands={"frequency": frequency, "amplitude": mT(1.2)})) ``` ## The laws, and building What acts on the body is attached, each law to the things it reads. The drag is the surrogate with lubrication, the torque is the field's on the body's moment, and gravity and the contact keep the body in the tube: *From `examples/script_a.py`:* ```python 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=field.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)) ``` `.build()` turns the declarations into a graph and a **build report** ([reading it](build_report.md)). The tutorial's file builds the reproduction arm and then the record arm, prints the values the report holds for water, and runs each arm for a short time. The length of the run is a constant, `show_water` prints the report's values with their sources, and `swim` runs the arm and reads the second half of its swim speed: *From `examples/tutorial_a.py`:* ```python #: A short run. Script A's reference window is the second half of a run of five seconds; this run is one second, so #: its speed is not that window's. SHORT = s(1.0) HALF = s(0.5) ``` *From `examples/tutorial_a.py`:* ```python def show_water(label, built): """The values the build read for water, each with where it came from.""" print(label) for value in built.report.values: if value.path.startswith("water."): print(f" {value.path} = {value.value} {value.unit}, {value.source.split(' (sha256')[0].split(',')[0]}") ``` *From `examples/tutorial_a.py`:* ```python def swim(label, built): """Run the short run, recording the swim speed's series, and read its second half.""" result = built.run(SHORT, record=["swim_speed:umr.speed"]) speed = result["swim_speed:umr"].speed.between(HALF).mean() print(f" swim speed over the second half of a {SHORT.value:g} s run: {speed.to(mm_per_s):.2f} mm/s") (watch,) = result.watches print(f" post-run watch on the drag: {watch.status}") ``` The last region builds the two arms from `examples/script_a.py` and calls those functions on each, which is the output below; the record arm it builds is described next. *From `examples/tutorial_a.py`:* ```python reproduction = script_a().build() show_water("reproduction arm: every value declared, with its reason", reproduction) swim("reproduction arm", reproduction) records = record_arm().build() show_water("record arm: water's values from OpenMaterica's record", records) swim("record arm", records) print(records.report.pending()) ``` ## The record arm The record arm replaces the declared water and magnet values with records, read at one temperature, and says what it accepts of each record that the record does not state: *From `examples/script_a.py`:* ```python # Water from OpenMaterica's example record, evaluated from the IAPWS formulations. Its viscosity states no shear # rate, so OpenMaterica answers "unknown": the experiment accepts that, with its reason. water_record = Record( "water", packaged_record("openmaterica_example_water"), require={"test_method": ("reference_formulation",)}, accept={"fluid.dynamic_viscosity": Acceptance( ("condition_unstated:shear_rate",), reason="IAPWS R12-08, from which the record's viscosity is evaluated, states no shear rate: A takes " "water as Newtonian at its shear rates, as its reference's constant viscosity did")}) water = experiment.add_medium(Medium("water", fills=tube.lumen, material=water_record)) # N45's remanence is a vendor's interval with nothing behind it: the experiment accepts what OpenMaterica cannot # check, and says which value inside the bounds it uses. n45 = Record( "n45", packaged_record("openmaterica_example_ndfeb_n45"), accept={"magnetic.remanence": Acceptance( ("stated_without_origin", "condition_unstated:temperature", "condition_unstated:specimen_shape", "condition_unstated:test_method"), reason="a vendor's limits for the grade, the only source at hand for these magnets (the 0.2 line names " "them as that vendor's 1 mm N45 cubes); they hold at 'room temperature' with no number, for no " "stated specimen or test method, and with no stated origin")}, nominal={"magnetic.remanence": Nominal( T(1.345), reason="the middle of the grade's limits: nothing measured places these magnets within them")}) ``` Run it from a source checkout of MIME, in an environment where MIME is installed (`examples/` is not part of the installed package): `python examples/tutorial_a.py`. It prints: ```text reproduction arm: every value declared, with its reason water.density = 1000.0 kg/m^3, declared in the experiment water.viscosity = 0.001 Pa s, declared in the experiment swim speed over the second half of a 1 s run: 3.17 mm/s post-run watch on the drag: above record arm: water's values from OpenMaterica's record water.density = 998.207 kg/m^3, record of material 'water' water.viscosity = 0.0010016 Pa s, record of material 'water' swim speed over the second half of a 1 s run: 3.18 mm/s post-run watch on the drag: above profile exploratory: nothing pending. ``` Read it from the top: - **Where each value came from.** In the reproduction arm water's values are `declared in the experiment`; in the record arm they are read from the record of the material `water`. The build report lists the same for every value an entry reads. - **The swim speed** is read from the observation's series, over the second half of the run. It is a short run's, so it is not the reference's window. The record arm changes three inputs at once (water's density, its viscosity, and the magnets' moment), so whatever the difference between the two printed speeds is, this run does not say which input moves it; `tests/script_a.py` switches the records on one at a time. - **`post-run watch on the drag: above`** is the watch of [the results note](../results.md): after a run, MIME compares the run's Reynolds number with the bound the drag entry declares itself valid up to, and here it finds it above. That is a flag on the result and never a refusal. The run is outside the range the entry declares itself for: the reproduction holds MIME 0.3 to the earlier line at the reference's operating point, and does not move that point into the range. - **The warning before the run** (on standard error, so not in the block above) is the same finding, said before anything runs: from the field's declared frequency, the hull's diameter and water's density and viscosity, the build computes the turning Reynolds number at the drive's rate and finds it far above the bound the surrogate rung declares. It is a warning in every profile, never a refusal. The rung's range is declared, not fitted: outside it the result is unvalidated, which is not the same as measured to be wrong. - **`profile exploratory: nothing pending`** is the stamp every output carries ([profiles](../profiles.md)). The record arm states its acceptances, so nothing is left pending. `python examples/script_a.py --explore` builds the same records without them and prints what sealing it takes. ## Where next The tutorial for experiment G waits for a1. For an arm and a controller, A0 is next: [tutorial A0](tutorial_a0.md).