diff --git a/Makefile b/Makefile index c273a3f..8e0e706 100644 --- a/Makefile +++ b/Makefile @@ -4,11 +4,19 @@ BLUE=\033[0;34m BLACK=\033[0;30m help: - @echo "$(BLUE) make dist - build dist files" + @echo "$(BLUE) make test - run the fast test suite" + @echo " make testall - run the full test suite, including slow notebook/example tests" + @echo " make dist - build dist files" @echo " make upload - upload to PyPI" @echo " make clean - remove dist and docs build files" @echo " make help - this message$(BLACK)" +test: .FORCE + pytest + +testall: .FORCE + pytest --runall + dist: .FORCE # $(MAKE) test python -m build diff --git a/RVC3/examples/imu_data.py b/RVC3/examples/imu_data.py index c7dbfdf..09beeb5 100755 --- a/RVC3/examples/imu_data.py +++ b/RVC3/examples/imu_data.py @@ -20,7 +20,7 @@ import matplotlib.pyplot as plt -def tumble(): +def IMU(): # accelerometer g0 = unitvec( [0, 0, 9.8] ).T gbias = 0.02 * np.r_[2, -2, 2].T # bias 2% of norm @@ -30,7 +30,7 @@ def tumble(): mbias = 0.02 * np.r_[-1, -1, 2] # bias 2# of norm # gyro - wbias = 0.05 * np.r_[-1, 2, -1] # bias 5% of max + wbias = 0.05 * np.r_[-1, 2, -1] # bias 10% of max ## simulation @@ -56,7 +56,7 @@ def tumble(): w0, t) # Solve for simulated sensor readings and true attitude - # 1 column per timestep + # 1 row per timestep am = np.zeros(omega.shape) mm = np.zeros(omega.shape) @@ -71,8 +71,9 @@ def tumble(): # add bias to measured wm = omega + wbias - data = namedtuple('tumble', 't omega_true attitude_true g B gyro accel magno') - return data(t, omega, truth, g0, m0, wm, am, mm) + imu = namedtuple('imu', 't dt gyro accel magno') + true = namedtuple('true', 't dt omega orientation g B') + return true(t, dt, omega, truth, g0, m0), imu(t, dt, wm, am, mm) if __name__ == "__main__": @@ -82,14 +83,14 @@ def plot(t, y, title): plt.grid(True) plt.title(title) - data = tumble() + true, imu = IMU() - print(data.attitude_true[100]) - print(data.attitude_true[100].rpy()) + print(true.orientation[100]) + print(true.orientation[100].rpy()) - plot(data.t, data.attitude_true.rpy(), 'attitude') - plot(data.t, data.gyro, 'gyro') - plot(data.t, data.accel, 'accel') - plot(data.t, data.magno, 'magno') + plot(true.t, true.orientation.rpy(), 'attitude') + plot(imu.t, imu.gyro, 'gyro') + plot(imu.t, imu.accel, 'accel') + plot(imu.t, imu.magno, 'magno') plt.show(block=True) \ No newline at end of file diff --git a/RVC3/examples/visodom.py b/RVC3/examples/visodom.py index cac2612..9e55f28 100755 --- a/RVC3/examples/visodom.py +++ b/RVC3/examples/visodom.py @@ -18,11 +18,15 @@ #!/usr/bin/env python3 # from RVC3.tools import rvcprint +import sys import numpy as np import matplotlib.pyplot as plt from machinevisiontoolbox import * import spatialmath.base as smb +# optional CLI arg: max number of frames to process, eg. `%run -m visodom 50` +max_frames = int(sys.argv[1]) if len(sys.argv) > 1 else None + # load .enpeda dataset, 12bit pixel values args = dict(mono=True, dtype="uint8", maxintval=4095, roi=[20, 750, 20, 480]) try: @@ -54,16 +58,18 @@ n_no_overlap = 0 n_optimized = 0 -for left, right in zip(lefts, rights): +for left_img, right_img in zip(lefts, rights): + if max_frames is not None and nframes >= max_frames: + break nframes += 1 - print("-----------------", left.id) + print("-----------------", left_img.id) # plt.clf() # plt.imshow(image.A, cmap='gray') # smb.plot_text((20, 420), f"frame {image.id}", color='w', backgroundcolor='k', fontsize=12) # find corner features - orbL = left.ORB(nfeatures=400, id="index") - orbR = right.ORB(nfeatures=400) + orbL = left_img.ORB(nfeatures=400, id="index") + orbR = right_img.ORB(nfeatures=400) # robustly match left and right corner features # - stereo match @@ -74,7 +80,7 @@ # too few/degenerate stereo correspondences to triangulate this # frame at all -- skip it entirely, keep the last good frame as # the temporal reference for the next one - print(f" stereo F estimation failed for frame {left.id}: {e}") + print(f" stereo F estimation failed for frame {left_img.id}: {e}") n_lr_fail += 1 continue print(matchLR) @@ -91,15 +97,17 @@ P, d = lines1.closest_to_line(lines2) print(np.nanmedian(d)) - if left.id > 0: + if left_img.id > 0: # if we have a previous frame # display two sequential stereo pairs plt.clf() - view4 = Image.Tile([left, right, left_prev, right_prev], columns=2, sep=0) + view4 = Image.Tile( + [left_img, right_img, left_prev, right_prev], columns=2, sep=0 + ) plt.imshow(view4.A, cmap="gray") - matchLR.plot_correspondence("y", offset=(left.width, 0), linewidth=0.5) + matchLR.plot_correspondence("y", offset=(left_img.width, 0), linewidth=0.5) # temporal matching matchFB = orbL.match(orbL_prev) @@ -107,18 +115,18 @@ F = matchFB.estimate(cam.points2F, method="ransac") print(matchFB) matchFB = matchFB.inliers # keep the inliers - matchFB.plot_correspondence("y", offset=(0, left.height), linewidth=0.5) + matchFB.plot_correspondence("y", offset=(0, left_img.height), linewidth=0.5) plt.pause(0.1) except ValueError as e: # too few/degenerate temporal correspondences -- same downstream # effect as zero overlapping landmarks below (no valid frame # motion estimate), but caught earlier since points2F() itself # can't even find a fundamental matrix here - print(f" temporal F estimation failed for frame {left.id}: {e}") + print(f" temporal F estimation failed for frame {left_img.id}: {e}") n_fb_fail += 1 matchFB = None - # if left.id == 10: + # if left_img.id == 10: # rvcprint.rvcprint(thicken=None) # now create a bundle adjustment problem, if we have a usable @@ -134,9 +142,7 @@ c_left = ba.add_view( SE3(), fixed=True ) # first camera at origin (current frame) - c_leftprev = ba.add_view( - SE3() - ) # initial guess, zero motion (prev frame) + c_leftprev = ba.add_view(SE3()) # initial guess, zero motion (prev frame) for k, Pk in enumerate(P.T): # for every 3D point from stereo if np.any(np.isnan(Pk)): @@ -148,9 +154,7 @@ continue landmark = ba.add_landmark(Pk) ba.add_projection(c_left, landmark, m.p1) # current left camera - ba.add_projection( - c_leftprev, landmark, m.p2 - ) # previous left camera + ba.add_projection(c_leftprev, landmark, m.p2) # previous left camera landmarks_added = True if landmarks_added: @@ -161,7 +165,7 @@ else: print( f" no overlapping stereo/temporal landmarks for frame " - f"{left.id} -- skipping bundle adjustment" + f"{left_img.id} -- skipping bundle adjustment" ) n_no_overlap += 1 displacements.append(np.full(6, np.nan)) @@ -169,8 +173,8 @@ # keep images and features for next cycle orbL_prev = orbL - left_prev = left - right_prev = right + left_prev = left_img + right_prev = right_img print() print("===== summary =====") diff --git a/RVC3/models/IBVS-holonomic.bd b/RVC3/models/IBVS-holonomic.bd index 3dd1f77..99c64d8 100644 --- a/RVC3/models/IBVS-holonomic.bd +++ b/RVC3/models/IBVS-holonomic.bd @@ -613,7 +613,7 @@ false ], [ - "args", + "fargs", [] ], [ diff --git a/RVC3/models/IBVS-nonholonomic.bd b/RVC3/models/IBVS-nonholonomic.bd index 43d9511..4fc5be3 100644 --- a/RVC3/models/IBVS-nonholonomic.bd +++ b/RVC3/models/IBVS-nonholonomic.bd @@ -299,11 +299,11 @@ false ], [ - "args", + "fargs", [] ], [ - "kwargs", + "fkwargs", {} ], [ @@ -491,11 +491,11 @@ false ], [ - "args", + "fargs", [] ], [ - "kwargs", + "fkwargs", {} ], [ @@ -572,11 +572,11 @@ true ], [ - "args", + "fargs", [] ], [ - "kwargs", + "fkwargs", {} ], [ @@ -996,7 +996,7 @@ false ], [ - "args", + "fargs", [] ], [ @@ -1162,7 +1162,7 @@ false ], [ - "args", + "fargs", [] ], [ diff --git a/RVC3/models/IBVS-quadrotor.bd b/RVC3/models/IBVS-quadrotor.bd index fd58f3a..ca14831 100644 --- a/RVC3/models/IBVS-quadrotor.bd +++ b/RVC3/models/IBVS-quadrotor.bd @@ -513,11 +513,11 @@ "=quadrotor" ], [ - "maxw", + "wmax", 1000 ], [ - "minw", + "wmin", 5 ], [ diff --git a/RVC3/models/braitenberg.py b/RVC3/models/braitenberg.py index 5883f6a..c022a65 100755 --- a/RVC3/models/braitenberg.py +++ b/RVC3/models/braitenberg.py @@ -52,7 +52,7 @@ def sensorfunc(x, offset): scale=[0, 100], size=5, shape="box", - trail=True, + path="b:", name="sensor field", init=background_graphics, ) diff --git a/RVC3/models/feedforward-main.py b/RVC3/models/feedforward-main.py index 2452012..77c5f10 100755 --- a/RVC3/models/feedforward-main.py +++ b/RVC3/models/feedforward-main.py @@ -28,5 +28,5 @@ import matplotlib.pyplot as plt -plt.plot(out.t, out.x[:, 1], out.t, out.y0[:, 1]) +plt.plot(out.t, out.x[:, 1], out.t, out.y[:, 1]) plt.show(block=True) diff --git a/RVC3/models/ploop_test.bd b/RVC3/models/ploop_test.bd new file mode 100644 index 0000000..beb5708 --- /dev/null +++ b/RVC3/models/ploop_test.bd @@ -0,0 +1,417 @@ +{ + "id": 140155287536736, + "created_by": "corkep", + "creation_time": 1632729537, + "simulation_time": 10.0, + "scene_width": 7168.000000000001, + "scene_height": 3785.6000000000004, + "blocks": [ + { + "id": 140271798589712, + "block_type": "SUBSYSTEM", + "title": "position loop", + "pos_x": -80.0, + "pos_y": -100.0, + "width": 200, + "height": 150, + "flipped": false, + "inputsNum": 3, + "outputsNum": 2, + "inputs": [ + { + "id": 140271798640800, + "index": 0, + "multi_wire": true, + "position": 1, + "socket_type": 1 + }, + { + "id": 140271798671248, + "index": 1, + "multi_wire": true, + "position": 1, + "socket_type": 1 + }, + { + "id": 140271798671728, + "index": 2, + "multi_wire": true, + "position": 1, + "socket_type": 1 + } + ], + "outputs": [ + { + "id": 140271799528944, + "index": 0, + "multi_wire": true, + "position": 3, + "socket_type": 2 + }, + { + "id": 140271798671488, + "index": 1, + "multi_wire": true, + "position": 3, + "socket_type": 2 + } + ], + "parameters": [ + [ + "subsys", + null + ], + [ + "nin", + 3 + ], + [ + "nout", + 2 + ], + [ + "blockargs", + null + ], + [ + "inport labels", + null + ], + [ + "outport labels", + null + ] + ] + }, + { + "id": 140271799528224, + "block_type": "LSPB", + "title": "trapezoidal trajectory", + "pos_x": -400.0, + "pos_y": -100.0, + "width": 100, + "height": 120.0, + "flipped": false, + "inputsNum": 0, + "outputsNum": 3, + "inputs": [], + "outputs": [ + { + "id": 140271799528320, + "index": 0, + "multi_wire": true, + "position": 3, + "socket_type": 2 + }, + { + "id": 140271799528272, + "index": 1, + "multi_wire": true, + "position": 3, + "socket_type": 2 + }, + { + "id": 140271799528512, + "index": 2, + "multi_wire": true, + "position": 3, + "socket_type": 2 + } + ], + "parameters": [ + [ + "q0", + null + ], + [ + "qf", + null + ], + [ + "T", + null + ], + [ + "blockargs", + null + ] + ] + }, + { + "id": 140271799602144, + "block_type": "CONSTANT", + "title": "0", + "pos_x": -400.0, + "pos_y": 60.0, + "width": 100, + "height": 100, + "flipped": false, + "inputsNum": 0, + "outputsNum": 1, + "inputs": [], + "outputs": [ + { + "id": 140271799602096, + "index": 0, + "multi_wire": true, + "position": 3, + "socket_type": 2 + } + ], + "parameters": [ + [ + "value", + "0" + ], + [ + "blockargs", + null + ] + ] + }, + { + "id": 140271799602336, + "block_type": "SCOPE", + "title": "q*, q", + "pos_x": 240.0, + "pos_y": -180.0, + "width": 100, + "height": 100, + "flipped": false, + "inputsNum": 2, + "outputsNum": 0, + "inputs": [ + { + "id": 140271799602432, + "index": 0, + "multi_wire": true, + "position": 1, + "socket_type": 1 + }, + { + "id": 140271799602768, + "index": 1, + "multi_wire": true, + "position": 1, + "socket_type": 1 + } + ], + "outputs": [], + "parameters": [ + [ + "nin", + 2 + ], + [ + "vector", + null + ], + [ + "styles", + null + ], + [ + "stairs", + false + ], + [ + "scale", + "auto" + ], + [ + "labels", + null + ], + [ + "grid", + true + ], + [ + "watch", + false + ], + [ + "blockargs", + null + ] + ] + }, + { + "id": 140271799619696, + "block_type": "SCOPE", + "title": "q error", + "pos_x": 240.0, + "pos_y": -20.0, + "width": 100, + "height": 100, + "flipped": false, + "inputsNum": 1, + "outputsNum": 0, + "inputs": [ + { + "id": 140271799620272, + "index": 0, + "multi_wire": true, + "position": 1, + "socket_type": 1 + } + ], + "outputs": [], + "parameters": [ + [ + "nin", + 1 + ], + [ + "vector", + null + ], + [ + "styles", + null + ], + [ + "stairs", + false + ], + [ + "scale", + "auto" + ], + [ + "labels", + null + ], + [ + "grid", + true + ], + [ + "watch", + false + ], + [ + "blockargs", + null + ] + ] + } + ], + "wires": [ + { + "id": 140271799600560, + "start_socket": 140271799528320, + "end_socket": 140271798640800, + "wire_type": 3, + "custom_routing": false, + "wire_coordinates": [] + }, + { + "id": 140271799602048, + "start_socket": 140271799528272, + "end_socket": 140271798671248, + "wire_type": 3, + "custom_routing": false, + "wire_coordinates": [] + }, + { + "id": 140271799602480, + "start_socket": 140271799602096, + "end_socket": 140271798671728, + "wire_type": 3, + "custom_routing": true, + "wire_coordinates": [ + [ + -80.0, + -20.0 + ], + [ + -120.0, + -20.0 + ], + [ + -120.0, + 100.0 + ], + [ + -300.0, + 100.0 + ] + ] + }, + { + "id": 140271799620512, + "start_socket": 140271799528944, + "end_socket": 140271799602768, + "wire_type": 3, + "custom_routing": false, + "wire_coordinates": [] + }, + { + "id": 140271799620944, + "start_socket": 140271798671488, + "end_socket": 140271799620272, + "wire_type": 3, + "custom_routing": false, + "wire_coordinates": [] + }, + { + "id": 140269547876656, + "start_socket": 140271799528320, + "end_socket": 140271799602432, + "wire_type": 3, + "custom_routing": true, + "wire_coordinates": [ + [ + 240.0, + -140.0 + ], + [ + -180.0, + -140.0 + ], + [ + -180.0, + -60.0 + ], + [ + -300.0, + -60.0 + ] + ] + } + ], + "labels": [ + { + "id": 140269547876416, + "text": "disturbance torque", + "pos_x": -280.0, + "pos_y": 100.0, + "width": 124, + "height": 24, + "fill_color": [ + 255, + 255, + 255, + 255 + ], + "styling": "\n\n

disturbance torque

" + }, + { + "id": 140269547877472, + "text": "velocity feedforward", + "pos_x": -270.0, + "pos_y": -40.0, + "width": 133, + "height": 24, + "fill_color": [ + 255, + 255, + 255, + 255 + ], + "styling": "\n\n

velocity feedforward

" + } + ], + "grouping_boxes": [] +} \ No newline at end of file diff --git a/RVC3/models/ploop_test.py b/RVC3/models/ploop_test.py new file mode 100755 index 0000000..6dd735d --- /dev/null +++ b/RVC3/models/ploop_test.py @@ -0,0 +1,51 @@ +#! /usr/bin/env python + +""" +Creates Fig 9.13 +Robotics, Vision & Control for Python, P. Corke, Springer 2023. +Copyright (c) 2021- Peter Corke +""" + +import site +import numpy as np +import bdsim + +from roboticstoolbox import quintic_func + +site.addsitedir("bdsim") + +from ploop import ploop, G + +# test harness +sim = bdsim.BDSim() +bd = sim.blockdiagram() + +# position = bd.quintic(0) HACK + +quinticfunc = quintic_func(0, 1, 1) +trajectory = bd.FUNCTION(quinticfunc, nout=3, name="quintic") +time = bd.TIME() + +taud = bd.CONSTANT(20 / G) +# speed = bd.WAVEFORM(wave='triangle', freq=2, amplitude=25) +PLOOP = bd.SUBSYSTEM(ploop, name="PLOOP") +theta_scope = bd.SCOPE(nin=2, name=r"$\theta$", labels=["actual", "demand"]) +werr_scope = bd.SCOPE(name=r"$\omega$ error") +tau_scope = bd.SCOPE(name=r"$\tau$") +wff_scope = bd.SCOPE(name=r"$\omega_{ff}$") + +bd.connect(time, trajectory) +bd.connect(trajectory[0], PLOOP[0], theta_scope[1]) +bd.connect(trajectory[1], PLOOP[1], wff_scope) +bd.connect(taud, PLOOP[2]) +bd.connect(PLOOP[0], theta_scope[0]) + +bd.connect(PLOOP[2], werr_scope) +bd.connect(PLOOP[3], tau_scope) + +bd.compile() # check the diagram + + +if __name__ == "__main__": + sim.report(bd) + out = sim.run(bd, 1, dt=1e-3) diff --git a/RVC3/models/vloop_test.bd b/RVC3/models/vloop_test.bd new file mode 100644 index 0000000..1dc5dfa --- /dev/null +++ b/RVC3/models/vloop_test.bd @@ -0,0 +1,625 @@ +{ + "id": 140192552973888, + "created_by": "corkep", + "creation_time": 1632727013, + "scene_width": 7168.000000000001, + "scene_height": 4368.0, + "blocks": [ + { + "id": 140588467244768, + "block_type": "SUBSYSTEM", + "title": "velocity loop", + "pos_x": -160.0, + "pos_y": -120.0, + "width": 200, + "height": 150, + "flipped": false, + "inputsNum": 3, + "outputsNum": 4, + "inputs": [ + { + "id": 140588467291760, + "index": 0, + "multi_wire": true, + "position": 1, + "socket_type": 1 + }, + { + "id": 140588467322208, + "index": 1, + "multi_wire": true, + "position": 1, + "socket_type": 1 + }, + { + "id": 140588467322688, + "index": 2, + "multi_wire": true, + "position": 1, + "socket_type": 1 + } + ], + "outputs": [ + { + "id": 140588467291808, + "index": 0, + "multi_wire": true, + "position": 3, + "socket_type": 2 + }, + { + "id": 140588467322448, + "index": 1, + "multi_wire": true, + "position": 3, + "socket_type": 2 + }, + { + "id": 140588467322640, + "index": 2, + "multi_wire": true, + "position": 3, + "socket_type": 2 + }, + { + "id": 140588473625424, + "index": 3, + "multi_wire": true, + "position": 3, + "socket_type": 2 + } + ], + "parameters": [ + [ + "subsys", + null + ], + [ + "nin", + 3 + ], + [ + "nout", + 4 + ], + [ + "blockargs", + null + ], + [ + "inport labels", + null + ], + [ + "outport labels", + null + ] + ] + }, + { + "id": 140588473655152, + "block_type": "INTERPOLATE", + "title": "velocity profile", + "pos_x": -380.0, + "pos_y": -120.0, + "width": 100, + "height": 100, + "flipped": false, + "inputsNum": 1, + "outputsNum": 1, + "inputs": [ + { + "id": 140588473655248, + "index": 0, + "multi_wire": true, + "position": 1, + "socket_type": 1 + } + ], + "outputs": [ + { + "id": 140588473655200, + "index": 0, + "multi_wire": true, + "position": 3, + "socket_type": 2 + } + ], + "parameters": [ + [ + "x", + null + ], + [ + "y", + null + ], + [ + "xy", + null + ], + [ + "time", + false + ], + [ + "kind", + "linear" + ], + [ + "blockargs", + null + ] + ] + }, + { + "id": 140588473690960, + "block_type": "SCOPE", + "title": "\u03c9", + "pos_x": 160.0, + "pos_y": -240.0, + "width": 100, + "height": 100, + "flipped": false, + "inputsNum": 2, + "outputsNum": 0, + "inputs": [ + { + "id": 140588473691056, + "index": 0, + "multi_wire": true, + "position": 1, + "socket_type": 1 + }, + { + "id": 140588473717424, + "index": 1, + "multi_wire": true, + "position": 1, + "socket_type": 1 + } + ], + "outputs": [], + "parameters": [ + [ + "nin", + 2 + ], + [ + "styles", + "None" + ], + [ + "scale", + "auto" + ], + [ + "labels", + [ + "w*", + "w" + ] + ], + [ + "grid", + "true" + ], + [ + "labels", + null + ], + [ + "grid", + true + ], + [ + "watch", + false + ], + [ + "blockargs", + null + ] + ] + }, + { + "id": 140588473691200, + "block_type": "SCOPE", + "title": "\u03c9 error", + "pos_x": 160.0, + "pos_y": -100.0, + "width": 100, + "height": 100, + "flipped": false, + "inputsNum": 1, + "outputsNum": 0, + "inputs": [ + { + "id": 140588473691488, + "index": 0, + "multi_wire": true, + "position": 1, + "socket_type": 1 + } + ], + "outputs": [], + "parameters": [ + [ + "nin", + 1 + ], + [ + "styles", + "None" + ], + [ + "scale", + "auto" + ], + [ + "labels", + "none" + ], + [ + "grid", + "true" + ], + [ + "labels", + null + ], + [ + "grid", + true + ], + [ + "watch", + false + ], + [ + "blockargs", + null + ] + ] + }, + { + "id": 140588473691632, + "block_type": "SCOPE", + "title": "motor torque", + "pos_x": 160.0, + "pos_y": 40.0, + "width": 100, + "height": 100, + "flipped": false, + "inputsNum": 1, + "outputsNum": 0, + "inputs": [ + { + "id": 140588473691920, + "index": 0, + "multi_wire": true, + "position": 1, + "socket_type": 1 + } + ], + "outputs": [], + "parameters": [ + [ + "nin", + 1 + ], + [ + "styles", + null + ], + [ + "scale", + "auto" + ], + [ + "labels", + null + ], + [ + "grid", + true + ], + [ + "labels", + null + ], + [ + "grid", + true + ], + [ + "watch", + false + ], + [ + "blockargs", + null + ] + ] + }, + { + "id": 140588473692064, + "block_type": "SCOPE", + "title": "integral term", + "pos_x": 160.0, + "pos_y": 180.0, + "width": 100, + "height": 100, + "flipped": false, + "inputsNum": 1, + "outputsNum": 0, + "inputs": [ + { + "id": 140588473688128, + "index": 0, + "multi_wire": true, + "position": 1, + "socket_type": 1 + } + ], + "outputs": [], + "parameters": [ + [ + "nin", + 1 + ], + [ + "styles", + null + ], + [ + "scale", + "auto" + ], + [ + "labels", + null + ], + [ + "grid", + true + ], + [ + "labels", + null + ], + [ + "grid", + true + ], + [ + "watch", + false + ], + [ + "blockargs", + null + ] + ] + }, + { + "id": 140588473720064, + "block_type": "CONSTANT", + "title": "disturbance torque", + "pos_x": -380.0, + "pos_y": 20.0, + "width": 100, + "height": 100, + "flipped": false, + "inputsNum": 0, + "outputsNum": 1, + "inputs": [], + "outputs": [ + { + "id": 140588473719776, + "index": 0, + "multi_wire": true, + "position": 3, + "socket_type": 2 + } + ], + "parameters": [ + [ + "value", + null + ], + [ + "blockargs", + null + ] + ] + }, + { + "id": 140588473719200, + "block_type": "CONSTANT", + "title": "feedforward torque", + "pos_x": -380.0, + "pos_y": 160.0, + "width": 100, + "height": 100, + "flipped": false, + "inputsNum": 0, + "outputsNum": 1, + "inputs": [], + "outputs": [ + { + "id": 140588473719440, + "index": 0, + "multi_wire": true, + "position": 3, + "socket_type": 2 + } + ], + "parameters": [ + [ + "value", + null + ], + [ + "blockargs", + null + ] + ] + }, + { + "id": 140237887245040, + "block_type": "TIME", + "title": "time", + "pos_x": -520.0, + "pos_y": -120.0, + "width": 100, + "height": 100, + "flipped": false, + "inputsNum": 0, + "outputsNum": 1, + "inputs": [], + "outputs": [ + { + "id": 140237887245136, + "index": 0, + "multi_wire": true, + "position": 3, + "socket_type": 2 + } + ], + "parameters": [ + [ + "blockargs", + null + ] + ] + } + ], + "wires": [ + { + "id": 140588473688992, + "start_socket": 140588473655200, + "end_socket": 140588467291760, + "wire_type": 3, + "custom_routing": false, + "wire_coordinates": [] + }, + { + "id": 140588473718960, + "start_socket": 140588467291808, + "end_socket": 140588473717424, + "wire_type": 3, + "custom_routing": false, + "wire_coordinates": [] + }, + { + "id": 140588473719296, + "start_socket": 140588467322448, + "end_socket": 140588473691488, + "wire_type": 3, + "custom_routing": false, + "wire_coordinates": [] + }, + { + "id": 140588473719392, + "start_socket": 140588467322640, + "end_socket": 140588473691920, + "wire_type": 3, + "custom_routing": false, + "wire_coordinates": [] + }, + { + "id": 140588473718624, + "start_socket": 140588473719776, + "end_socket": 140588467322208, + "wire_type": 3, + "custom_routing": false, + "wire_coordinates": [] + }, + { + "id": 140237887272704, + "start_socket": 140237887245136, + "end_socket": 140588473655248, + "wire_type": 3, + "custom_routing": false, + "wire_coordinates": [] + }, + { + "id": 140192616650400, + "start_socket": 140588473655200, + "end_socket": 140588473691056, + "wire_type": 3, + "custom_routing": true, + "wire_coordinates": [ + [ + 160.0, + -200.0 + ], + [ + -220.0, + -200.0 + ], + [ + -220.0, + -80.0 + ], + [ + -280.0, + -80.0 + ] + ] + }, + { + "id": 140192616650544, + "start_socket": 140588473625424, + "end_socket": 140588473688128, + "wire_type": 3, + "custom_routing": true, + "wire_coordinates": [ + [ + 160.0, + 220.0 + ], + [ + 80.0, + 220.0 + ], + [ + 80.0, + -20.0 + ], + [ + 40.0, + -20.0 + ] + ] + }, + { + "id": 140192616650688, + "start_socket": 140588473719440, + "end_socket": 140588467322688, + "wire_type": 3, + "custom_routing": true, + "wire_coordinates": [ + [ + -160.0, + -40.0 + ], + [ + -200.0, + -40.0 + ], + [ + -200.0, + 200.0 + ], + [ + -280.0, + 200.0 + ] + ] + } + ], + "labels": [], + "grouping_boxes": [] +} \ No newline at end of file diff --git a/RVC3/models/vloop_test.py b/RVC3/models/vloop_test.py new file mode 100755 index 0000000..ecd06a7 --- /dev/null +++ b/RVC3/models/vloop_test.py @@ -0,0 +1,58 @@ +#! /usr/bin/env python + +""" +Creates Fig 9.8 +Robotics, Vision & Control for Python, P. Corke, Springer 2023. +Copyright (c) 2021- Peter Corke +""" + +import numpy as np +import bdsim + +import site +import numpy as np +import bdsim + +from vloop import vloop, B + + +# test harness for velocity loop +sim = bdsim.BDSim(graphics=True, progress=False) +bd = sim.blockdiagram() + +# parameters +disturbance = bd.CONSTANT(0, name="disturbance") +feedforward = bd.CONSTANT(0) +# speed = bd.WAVEFORM(wave='triangle', freq=2, amplitude=25) +speed = bd.INTERPOLATE( + x=(0, 0.1, 0.3, 0.4, 0.6, 1), y=(0, 0, 50, 50, 0, 0), time=True, name="demand" +) + +# import the velocity loop +VLOOP = bd.SUBSYSTEM(vloop, name="VLOOP") + +# scopes +w_scope = bd.SCOPE(nin=2, name=r"$\omega$", labels=["actual", "demand"]) +werr_scope = bd.SCOPE(name=r"$\omega_{err}$") +tau_scope = bd.SCOPE(name=r"$\tau$") +integral_scope = bd.SCOPE(name="integral") + +demand_scope = bd.SCOPE(name="demand scope") + +bd.connect(speed, demand_scope) + +bd.connect(VLOOP[0], w_scope[0]) +bd.connect(VLOOP[1], werr_scope) +bd.connect(VLOOP[2], tau_scope) +bd.connect(VLOOP[3], integral_scope) + +bd.connect(disturbance, VLOOP[1]) +bd.connect(speed, VLOOP[0], w_scope[1]) +bd.connect(feedforward, VLOOP[2]) + +bd.compile() # check the diagram + +if __name__ == "__main__": + sim.report(bd) + + out = sim.run(bd, 1, dt=1e-3) diff --git a/errata.md b/errata.md index 49f55bb..b5845a8 100644 --- a/errata.md +++ b/errata.md @@ -493,3 +493,103 @@ from roboticstoolbox.models.URDF.URDFRobot import URDF_read urdf, *_ = URDF_read("ur5") urdf ``` + +## §14.8.3 — `visodom.py` limited to 50 frames by default + +**Notebook:** `chap14.ipynb` + +**Reason:** The bridge dataset has 251 stereo frame pairs; processing all +of them (ORB matching + bundle adjustment per frame) takes several +minutes. Added an optional frame-count argument to +`RVC3/examples/visodom.py` (processes every frame if omitted) and the +notebook now passes 50 for a much faster run. Increase or drop the +argument to process more frames. + +**As printed:** +```python +%run -m visodom +``` + +**Current toolbox syntax:** +```python +%run -m visodom 50 +``` + +## §15.2.2 — bdsim clock log field renamed from `.x` to `.X` + +**Notebook:** `chap15.ipynb` + +**Reason:** bdsim's per-clock logged state array was renamed from a +lowercase `.x` attribute to `.X`, to match the uppercase convention +used for state-array fields elsewhere in a run's output struct (eg. +the top-level `out.x`/`out.xnames` pairing uses lowercase for the +continuous state but clocked/discrete state blocks use the +uppercase form). + +**As printed:** +```python +plt.plot(out.clock0.t, out.clock0.x) +``` + +**Current toolbox syntax:** +```python +plt.plot(out.clock0.t, out.clock0.X) +``` + +## §2.3.1.2 — `tripleangledemo` is now an installed CLI command + +**Notebook:** `chap2.ipynb` + +**Reason:** RTB used to ship `tripleangledemo.py` as a standalone +script; it's now `roboticstoolbox.demo.tripleangledemo`, exposed as +an installed console command (`[project.scripts]` entry point in +RTB's `pyproject.toml`) rather than a bare top-level module, so +`%run -m tripleangledemo` can no longer find it +(`'tripleangledemo' is not a valid modulename on sys.path`). Invoke +it as the installed command directly instead. + +**As printed:** +```python +%run -m tripleangledemo +``` + +**Current toolbox syntax:** +```python +!tripleangledemo +``` + +## §2.4.8 — `twistdemo` is now an installed CLI command + +**Notebook:** `chap2.ipynb` + +**Reason:** Same change as `tripleangledemo` above -- +`twistdemo.py` is now `roboticstoolbox.demo.twistdemo`, an installed +console command rather than a bare top-level module. + +**As printed:** +```python +%run -m twistdemo +``` + +**Current toolbox syntax:** +```python +!twistdemo +``` + +## §B.2.1 — `eigdemo` is now an installed CLI command + +**Notebook:** `app.ipynb` + +**Reason:** Same change as `tripleangledemo`/`twistdemo` above -- +`eigdemo.py` is now `roboticstoolbox.demo.eigdemo`, an installed +console command rather than a bare top-level module. + +**As printed:** +```python +%run -m eigdemo 1 2 3 4 +``` + +**Current toolbox syntax:** +```python +!eigdemo 1 2 3 4 +``` diff --git a/figures/code/chapter3/fig3_17.py b/figures/code/chapter3/fig3_17.py index 1730d16..eef7c58 100755 --- a/figures/code/chapter3/fig3_17.py +++ b/figures/code/chapter3/fig3_17.py @@ -10,7 +10,7 @@ from spatialmath.base import * from spatialmath.base import sym from spatialmath import SE3, SO2, SO3, UnitQuaternion -from rvcprint from RVC3.tools import rvcprint +from RVC3.tools import rvcprint # load the simulation data from imu_data import IMU diff --git a/notebooks/app.ipynb b/notebooks/app.ipynb index 8efbe43..ad300b4 100644 --- a/notebooks/app.ipynb +++ b/notebooks/app.ipynb @@ -23,7 +23,23 @@ "id": "1", "metadata": {}, "outputs": [], - "source": "try:\n from google.colab import output\n print('Running on CoLab')\n output.enable_custom_widget_manager()\n !pip install ipympl\n !pip install spatialmath-python\n COLAB = True\n SWIFT = False\nexcept ModuleNotFoundError:\n COLAB = False\n SWIFT = False\nfrom IPython.core.interactiveshell import InteractiveShell\nInteractiveShell.ast_node_interactivity = \"last_expr_or_assign\"\n\n%matplotlib widget" + "source": [ + "try:\n", + " from google.colab import output\n", + " print('Running on CoLab')\n", + " output.enable_custom_widget_manager()\n", + " !pip install ipympl\n", + " !pip install spatialmath-python\n", + " COLAB = True\n", + " SWIFT = False\n", + "except ModuleNotFoundError:\n", + " COLAB = False\n", + " SWIFT = False\n", + "from IPython.core.interactiveshell import InteractiveShell\n", + "InteractiveShell.ast_node_interactivity = \"last_expr_or_assign\"\n", + "\n", + "%matplotlib widget" + ] }, { "cell_type": "code", @@ -31,7 +47,28 @@ "id": "2", "metadata": {}, "outputs": [], - "source": "from IPython.display import HTML\n\n# add RTB examples folder to the path\nimport sys\nimport os.path\nimport roboticstoolbox as rtb\nsys.path.append(os.path.join(rtb.__path__[0], 'examples'))\n\n# standard imports\nimport numpy as np\nfrom scipy import linalg\nimport matplotlib.pyplot as plt\nimport math\nfrom math import pi\nnp.set_printoptions(\n linewidth=120, formatter={\n 'float': lambda x: f\"{0:8.4g}\" if abs(x) < 1e-10 else f\"{x:8.4g}\"})\nnp.random.seed(0)\nfrom spatialmath import *\nfrom spatialmath.base import *" + "source": [ + "from IPython.display import HTML\n", + "\n", + "# add RTB examples folder to the path\n", + "import sys\n", + "import os.path\n", + "import roboticstoolbox as rtb\n", + "sys.path.append(os.path.join(rtb.__path__[0], 'examples'))\n", + "\n", + "# standard imports\n", + "import numpy as np\n", + "from scipy import linalg\n", + "import matplotlib.pyplot as plt\n", + "import math\n", + "from math import pi\n", + "np.set_printoptions(\n", + " linewidth=120, formatter={\n", + " 'float': lambda x: f\"{0:8.4g}\" if abs(x) < 1e-10 else f\"{x:8.4g}\"})\n", + "np.random.seed(0)\n", + "from spatialmath import *\n", + "from spatialmath.base import *" + ] }, { "cell_type": "markdown", @@ -76,7 +113,7 @@ "metadata": {}, "outputs": [], "source": [ - "%run -m eigdemo 1 2 3 4" + "!eigdemo 1 2 3 4" ] }, { @@ -234,7 +271,9 @@ "id": "23", "metadata": {}, "outputs": [], - "source": "e, v = np.linalg.eigh(E)" + "source": [ + "e, v = np.linalg.eigh(E)" + ] }, { "cell_type": "markdown", @@ -825,7 +864,9 @@ "id": "79", "metadata": {}, "outputs": [], - "source": "g[1].neighbours()" + "source": [ + "g[1].neighbours()" + ] }, { "cell_type": "code", @@ -1080,4 +1121,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/chap14.ipynb b/notebooks/chap14.ipynb index a86b71f..268a7da 100644 --- a/notebooks/chap14.ipynb +++ b/notebooks/chap14.ipynb @@ -23,7 +23,20 @@ "id": "1", "metadata": {}, "outputs": [], - "source": "try:\n import google.colab\n print('Running on CoLab')\n !pip install matplotlib\n !pip install machinevision-toolbox-python\n !pip install --no-deps rvc3python\n COLAB = True\nexcept:\n COLAB = False\n\nfrom IPython.core.interactiveshell import InteractiveShell\nInteractiveShell.ast_node_interactivity = \"last_expr_or_assign\"" + "source": [ + "try:\n", + " import google.colab\n", + " print('Running on CoLab')\n", + " !pip install matplotlib\n", + " !pip install machinevision-toolbox-python\n", + " !pip install --no-deps rvc3python\n", + " COLAB = True\n", + "except:\n", + " COLAB = False\n", + "\n", + "from IPython.core.interactiveshell import InteractiveShell\n", + "InteractiveShell.ast_node_interactivity = \"last_expr_or_assign\"" + ] }, { "cell_type": "code", @@ -31,7 +44,27 @@ "id": "2", "metadata": {}, "outputs": [], - "source": "from IPython.core.display import HTML\n\nimport RVC3 as rvc\nimport sys, os.path\nsys.path.append(os.path.join(rvc.__path__[0], 'examples'))\n\nimport numpy as np\nfrom scipy import linalg, stats\nimport matplotlib.pyplot as plt\nimport math\nfrom math import pi\nnp.set_printoptions(\n linewidth=120, formatter={\n 'float': lambda x: f\"{0:8.4g}\" if abs(x) < 1e-10 else f\"{x:8.4g}\"})\nnp.random.seed(0)\nfrom machinevisiontoolbox.base import *\nfrom machinevisiontoolbox import *\nfrom spatialmath.base import *\nfrom spatialmath import *" + "source": [ + "from IPython.core.display import HTML\n", + "\n", + "import RVC3 as rvc\n", + "import sys, os.path\n", + "sys.path.append(os.path.join(rvc.__path__[0], 'examples'))\n", + "\n", + "import numpy as np\n", + "from scipy import linalg, stats\n", + "import matplotlib.pyplot as plt\n", + "import math\n", + "from math import pi\n", + "np.set_printoptions(\n", + " linewidth=120, formatter={\n", + " 'float': lambda x: f\"{0:8.4g}\" if abs(x) < 1e-10 else f\"{x:8.4g}\"})\n", + "np.random.seed(0)\n", + "from machinevisiontoolbox.base import *\n", + "from machinevisiontoolbox import *\n", + "from spatialmath.base import *\n", + "from spatialmath import *" + ] }, { "cell_type": "markdown", @@ -1916,7 +1949,15 @@ "id": "181", "metadata": {}, "outputs": [], - "source": "if not COLAB:\n bunny_pcd = PointCloud.Read('bunny.ply')\n bunny_pcd.disp(block=True)\n pcd = bunny_pcd.voxel_grid(voxel_size=0.01).disp(block=True)\n pcd = bunny_pcd.downsample_voxel(voxel_size=0.01)\n pcd.normals(radius=0.1, max_nn=30)\n pcd.disp(block=True)" + "source": [ + "if not COLAB:\n", + " bunny_pcd = PointCloud.Read('bunny.ply')\n", + " bunny_pcd.disp(block=True)\n", + " pcd = bunny_pcd.voxel_grid(voxel_size=0.01).disp(block=True)\n", + " pcd = bunny_pcd.downsample_voxel(voxel_size=0.01)\n", + " pcd.normals(radius=0.1, max_nn=30)\n", + " pcd.disp(block=True)" + ] }, { "cell_type": "markdown", @@ -2142,7 +2183,10 @@ "id": "201", "metadata": {}, "outputs": [], - "source": "composite = composite.paste(images[0], (0, 0));\ncomposite.disp();" + "source": [ + "composite = composite.paste(images[0], (0, 0));\n", + "composite.disp();" + ] }, { "cell_type": "code", @@ -2184,7 +2228,10 @@ "id": "205", "metadata": {}, "outputs": [], - "source": "composite = composite.paste(tile, topleft, method=\"blend\");\ncomposite.disp();" + "source": [ + "composite = composite.paste(tile, topleft, method=\"blend\");\n", + "composite.disp();" + ] }, { "cell_type": "markdown", @@ -2237,7 +2284,13 @@ "cell_type": "markdown", "id": "210", "metadata": {}, - "source": "
\n\n**Warning:** The commented out code produces an animation for a Python script, however, using Jupyter it produces a set of separate images. Use OpenCV instead (`matplotlib=False`) to display the animation in a separate window.\n\n
" + "source": [ + "
\n", + "\n", + "**Warning:** The commented out code produces an animation for a Python script, however, using Jupyter it produces a set of separate images. Use OpenCV instead (`matplotlib=False`) to display the animation in a separate window.\n", + "\n", + "
" + ] }, { "cell_type": "code", @@ -2276,7 +2329,7 @@ "metadata": {}, "outputs": [], "source": [ - "%run -m visodom" + "%run -m visodom 50" ] }, { @@ -2326,4 +2379,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/chap15.ipynb b/notebooks/chap15.ipynb index 245fedc..a3d77af 100644 --- a/notebooks/chap15.ipynb +++ b/notebooks/chap15.ipynb @@ -43,7 +43,26 @@ "id": "2", "metadata": {}, "outputs": [], - "source": "import numpy as np\nfrom scipy import linalg\nimport matplotlib.pyplot as plt\nimport math\nfrom math import pi\nnp.set_printoptions(\n linewidth=120, formatter={\n 'float': lambda x: f\"{0:8.4g}\" if abs(x) < 1e-10 else f\"{x:8.4g}\"})\nnp.random.seed(0)\n\nfrom machinevisiontoolbox.base import *\nfrom machinevisiontoolbox import *\nfrom spatialmath.base import *\nfrom spatialmath import *\n\nimport sys, os.path\nimport RVC3 as rvc\nmodels_dir = os.path.join(rvc.__path__[0], 'models')" + "source": [ + "import numpy as np\n", + "from scipy import linalg\n", + "import matplotlib.pyplot as plt\n", + "import math\n", + "from math import pi\n", + "np.set_printoptions(\n", + " linewidth=120, formatter={\n", + " 'float': lambda x: f\"{0:8.4g}\" if abs(x) < 1e-10 else f\"{x:8.4g}\"})\n", + "np.random.seed(0)\n", + "\n", + "from machinevisiontoolbox.base import *\n", + "from machinevisiontoolbox import *\n", + "from spatialmath.base import *\n", + "from spatialmath import *\n", + "\n", + "import sys, os.path\n", + "import RVC3 as rvc\n", + "models_dir = os.path.join(rvc.__path__[0], 'models')" + ] }, { "cell_type": "markdown", @@ -503,7 +522,9 @@ "id": "48", "metadata": {}, "outputs": [], - "source": "%run -i $models_dir/IBVS-main.py -H" + "source": [ + "%run -i $models_dir/IBVS-main.py -H" + ] }, { "cell_type": "code", @@ -532,7 +553,7 @@ "metadata": {}, "outputs": [], "source": [ - "plt.plot(out.clock0.t, out.clock0.x)" + "plt.plot(out.clock0.t, out.clock0.X)" ] }, { @@ -774,4 +795,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/chap2.ipynb b/notebooks/chap2.ipynb index bed9494..f3c9979 100644 --- a/notebooks/chap2.ipynb +++ b/notebooks/chap2.ipynb @@ -24,7 +24,23 @@ "id": "1", "metadata": {}, "outputs": [], - "source": "try:\n from google.colab import output\n print('Running on CoLab')\n output.enable_custom_widget_manager()\n !pip install ipympl\n !pip install spatialmath-python\n COLAB = True\n SWIFT = False\nexcept ModuleNotFoundError:\n COLAB = False\n SWIFT = False\nfrom IPython.core.interactiveshell import InteractiveShell\nInteractiveShell.ast_node_interactivity = \"last_expr_or_assign\"\n\n%matplotlib widget" + "source": [ + "try:\n", + " from google.colab import output\n", + " print('Running on CoLab')\n", + " output.enable_custom_widget_manager()\n", + " !pip install ipympl\n", + " !pip install spatialmath-python\n", + " COLAB = True\n", + " SWIFT = False\n", + "except ModuleNotFoundError:\n", + " COLAB = False\n", + " SWIFT = False\n", + "from IPython.core.interactiveshell import InteractiveShell\n", + "InteractiveShell.ast_node_interactivity = \"last_expr_or_assign\"\n", + "\n", + "%matplotlib widget" + ] }, { "cell_type": "code", @@ -32,7 +48,22 @@ "id": "2", "metadata": {}, "outputs": [], - "source": "from IPython.display import HTML\n\n# standard imports\nimport numpy as np\nfrom scipy import linalg\nimport matplotlib.pyplot as plt\nimport math\nfrom math import pi\nnp.set_printoptions(\n linewidth=120, formatter={\n 'float': lambda x: f\"{0:8.4g}\" if abs(x) < 1e-10 else f\"{x:8.4g}\"})\nnp.random.seed(0)\nfrom spatialmath import *\nfrom spatialmath.base import *" + "source": [ + "from IPython.display import HTML\n", + "\n", + "# standard imports\n", + "import numpy as np\n", + "from scipy import linalg\n", + "import matplotlib.pyplot as plt\n", + "import math\n", + "from math import pi\n", + "np.set_printoptions(\n", + " linewidth=120, formatter={\n", + " 'float': lambda x: f\"{0:8.4g}\" if abs(x) < 1e-10 else f\"{x:8.4g}\"})\n", + "np.random.seed(0)\n", + "from spatialmath import *\n", + "from spatialmath.base import *" + ] }, { "attachments": {}, @@ -604,7 +635,20 @@ "cell_type": "markdown", "id": "59", "metadata": {}, - "source": "
\n\n**Note:** Robust, portable animation in Jupyter notebooks is challenging. Here we use an option to `tranimate` that causes it to return the animation as a snippet of HTML5 which is then displayed\n```\nHTML(tranimate(R, movie=True))\n```\nIf you wish to animate a coordinate frame from a regular Python script use the simpler syntax\n```\ntranimate(R)\n```\n\n
" + "source": [ + "
\n", + "\n", + "**Note:** Robust, portable animation in Jupyter notebooks is challenging. Here we use an option to `tranimate` that causes it to return the animation as a snippet of HTML5 which is then displayed\n", + "```\n", + "HTML(tranimate(R, movie=True))\n", + "```\n", + "If you wish to animate a coordinate frame from a regular Python script use the simpler syntax\n", + "```\n", + "tranimate(R)\n", + "```\n", + "\n", + "
" + ] }, { "cell_type": "code", @@ -793,7 +837,15 @@ "cell_type": "markdown", "id": "78", "metadata": {}, - "source": "
\n\n**Warning:** The next cell will launch an interactive tool (using the Swift visualizer) in a new browser tab. Close the browser tab when you are done with it. \n\nYou might also have to stop the cell from executing, by pressing the stop button for the cell. It may terminate with lots of errors, don't panic.\n\n
" + "source": [ + "
\n", + "\n", + "**Warning:** The next cell will launch an interactive tool (using the Swift visualizer) in a new browser tab. Close the browser tab when you are done with it. \n", + "\n", + "You might also have to stop the cell from executing, by pressing the stop button for the cell. It may terminate with lots of errors, don't panic.\n", + "\n", + "
" + ] }, { "cell_type": "code", @@ -805,7 +857,7 @@ "if COLAB or not SWIFT:\n", " print(\"we can't run this demo from the Colab environment (yet)\")\n", "else:\n", - " %run -m tripleangledemo" + " !tripleangledemo" ] }, { @@ -1683,7 +1735,15 @@ "cell_type": "markdown", "id": "168", "metadata": {}, - "source": "
\n\n**Warning:** The next cell will launch an interactive tool (using the Swift visualizer) in a new browser tab. Close the browser tab when you are done with it. \n\nYou might also have to stop the cell from executing, by pressing the stop button for the cell. It may terminate with lots of errors, don't panic.\n\n
" + "source": [ + "
\n", + "\n", + "**Warning:** The next cell will launch an interactive tool (using the Swift visualizer) in a new browser tab. Close the browser tab when you are done with it. \n", + "\n", + "You might also have to stop the cell from executing, by pressing the stop button for the cell. It may terminate with lots of errors, don't panic.\n", + "\n", + "
" + ] }, { "cell_type": "code", @@ -1695,7 +1755,7 @@ "if COLAB or not SWIFT:\n", " print(\"we can't run this demo from the Colab environment (yet)\")\n", "else:\n", - " %run -m twistdemo" + " !twistdemo" ] }, { @@ -1911,4 +1971,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/tests/notebook_runner.py b/tests/notebook_runner.py index b522bda..267f2ac 100644 --- a/tests/notebook_runner.py +++ b/tests/notebook_runner.py @@ -112,6 +112,9 @@ def classify_outputs(outputs: list) -> tuple[str, str]: def run_notebook(path: Path, skiplist: list[dict[str, str]], timeout: int) -> NotebookResult: + os.environ.setdefault("MPLBACKEND", "Agg") # never pop real GUI windows during an automated run + os.environ.setdefault("BDSIM_NO_GRAPHICS", "1") # unconditionally disables bdsim animation/graphics/movies + result = NotebookResult(name=path.name) try: @@ -167,6 +170,19 @@ def write_report(results: list[NotebookResult], out_path: Path) -> None: lines = ["# Notebook test run\n"] totals = {"clean": 0, "warning": 0, "error": 0, "skipped": 0} + # Concise pass/fail-per-notebook summary up top -- cell numbers below + # aren't meaningful to a reader (Jupyter doesn't show them), so lead + # with what actually matters: which notebooks are clean. + summary_lines = ["## Summary\n"] + for r in results: + if r.load_error: + summary_lines.append(f"- **{r.name}**: FAILED TO LOAD/RUN") + continue + counts = r.counts() + status = "clean" if counts["error"] == 0 else f"{counts['error']} error(s)" + summary_lines.append(f"- **{r.name}**: {status}") + summary_lines.append("") + for r in results: counts = r.counts() for k, v in counts.items(): @@ -185,16 +201,17 @@ def write_report(results: list[NotebookResult], out_path: Path) -> None: if cell.status == "clean": continue marker = {"warning": "WARN", "error": "FAIL", "skipped": "SKIP"}[cell.status] - lines.append(f"- [{marker}] cell[{cell.index}] `{cell.source_preview}`") + lines.append(f"- [{marker}] `{cell.source_preview}`") if cell.detail: lines.append(f" {cell.detail}") lines.append("") - lines.insert( - 1, + header_lines = [ f"**Totals:** {totals['clean']} clean, {totals['warning']} warning, " f"{totals['error']} error, {totals['skipped']} skipped\n", - ) + *summary_lines, + ] + lines[1:1] = header_lines out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text("\n".join(lines)) @@ -207,8 +224,6 @@ def main() -> int: parser.add_argument("--report", default=str(REPORTS_DIR / "latest.md"), help="output report path") args = parser.parse_args() - os.environ.setdefault("MPLBACKEND", "Agg") # never pop real GUI windows during an automated run - if args.notebooks: paths = [Path(p) for p in args.notebooks] else: diff --git a/tests/skiplist.yaml b/tests/skiplist.yaml index b45d510..e2e7d97 100644 --- a/tests/skiplist.yaml +++ b/tests/skiplist.yaml @@ -11,6 +11,10 @@ # Match is a plain substring test against the cell's joined source -- keep # patterns specific enough not to accidentally match unrelated cells. +chap10.ipynb: + - match: "theta = esttheta(im)" + reason: "esttheta() calls plt.ginput() to let the user click a region on the image -- under a headless Agg backend there's no display to click on, so ginput() returns no points and Polygon2() gets an empty array instead of a real polygon. Same 'requires real user interaction' category as the ginput() cell in the next markdown block." + chap11.ipynb: - match: "VideoCamera(0)" reason: "no physical webcam available in this environment" diff --git a/tests/test_notebooks.py b/tests/test_notebooks.py index 2804828..e977378 100644 --- a/tests/test_notebooks.py +++ b/tests/test_notebooks.py @@ -37,7 +37,10 @@ def test_notebook(notebook_name: str): assert result.load_error is None, result.load_error counts = result.counts() errors = [c for c in result.cells if c.status == "error"] - detail = "; ".join(f"cell[{c.index}] {c.detail}" for c in errors) + # Cell numbers aren't meaningful to a reader -- Jupyter doesn't show + # them. Just the error itself; see tests/reports/latest.md for the + # per-cell source preview if more context is needed. + detail = "; ".join(c.detail for c in errors) assert counts["error"] == 0, detail