From accba627c06ea5c173f8235170be5d6947cbc3b1 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 17 Aug 2026 10:37:39 +1000 Subject: [PATCH 01/13] fix: commit vloop_test.py/.bd and ploop_test.py/.bd model fixtures These were never actually tracked in git -- they only "worked" because they sat as untracked files in local working directories, invisible under the old blanket `*test*` .gitignore rule removed in #40. A clean checkout doesn't have them, which breaks: - rvctool --test's bdsim check (imports vloop_test directly) - figures/code/chapter9/fig9_13.py, fig9_14.py (import ploop_test) - notebooks/scripts/chap9.py (%run -m ploop_test -H) --- RVC3/models/ploop_test.bd | 417 +++++++++++++++++++++++++ RVC3/models/ploop_test.py | 51 ++++ RVC3/models/vloop_test.bd | 625 ++++++++++++++++++++++++++++++++++++++ RVC3/models/vloop_test.py | 58 ++++ 4 files changed, 1151 insertions(+) create mode 100644 RVC3/models/ploop_test.bd create mode 100755 RVC3/models/ploop_test.py create mode 100644 RVC3/models/vloop_test.bd create mode 100755 RVC3/models/vloop_test.py 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) From 877059455d4e811977274b2cedde19bf23182182 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 17 Aug 2026 15:29:21 +1000 Subject: [PATCH 02/13] fix: chap3 -- restore imu_data.IMU() with correct true/imu API RVC3/examples/imu_data.py had been reconstructed from ex_tumble.py (a different, incompatible script) rather than the canonical source already tracked at figures/code/chapter3/imu_data.py, leaving it with a tumble() function/single-namedtuple shape that doesn't match what chap3.ipynb actually calls (IMU() -> (true, imu) two-namedtuple pair). Copied the correct implementation over and fixed its own stale __main__ self-test block, which still referenced the old tumble() name and field layout. Also fixes an unrelated pre-existing syntax error in figures/code/chapter3/fig3_17.py ("from rvcprint from RVC3.tools import rvcprint", present since the file's original commit). Co-Authored-By: Claude Sonnet 5 --- RVC3/examples/imu_data.py | 25 +++++++++++++------------ figures/code/chapter3/fig3_17.py | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) 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/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 From 6af21844128ff9d90aef16a028337be230f5aec4 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 17 Aug 2026 16:02:18 +1000 Subject: [PATCH 03/13] fix: chap5 -- braitenberg.py VEHICLEPLOT trail= kwarg no longer exists bdsim's VehiclePlot block dropped the boolean trail= option in favour of path= (a linestyle string/dict), matching driveconfig.py and drivepoint.py which already use path="b:". braitenberg.py was still passing the old trail=True, which errored with "unexpected keyword arguments ['trail']". Co-Authored-By: Claude Sonnet 5 --- RVC3/models/braitenberg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, ) From f85edc509f8d721801cf5fe303da3abe4fa55561 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 17 Aug 2026 20:11:14 +1000 Subject: [PATCH 04/13] fix: chap10 -- skiplist esttheta()'s ginput() cell, can't run headless The AttributeError (spatialmath.base.plot_poly no longer exists, renamed to plot_polygon) is fixed upstream: MVTB's esttheta() was rewritten in petercorke/machinevision-toolbox-python#82 to use spatialmath.Polygon2 directly, already merged to MVTB main. What's left is structural, not a bug: esttheta() calls plt.ginput() to let the user click a region on the displayed image. Under the headless Agg backend used for automated testing there's no display to click on, so ginput() returns zero points and Polygon2() gets an empty array. Same category as the neighbouring markdown-flagged ginput() cell and the swift/PointCloud entries already in this file. Co-Authored-By: Claude Sonnet 5 --- tests/skiplist.yaml | 4 ++++ 1 file changed, 4 insertions(+) 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" From f85ac9aa73b497cf796ffc63afa82dac5b74f796 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 17 Aug 2026 21:19:04 +1000 Subject: [PATCH 05/13] fix: chap14 -- visodom.py namespace clobbering, timestamps.dat path, frame limit The %run -m visodom cell crashed on cv2.findFundamentalMat's raw cv2.error (fixed upstream, machinevisiontoolbox-python#101), which masked two further bugs only reachable once that run completes: - visodom.py's loop reused the variable names left/right, which %run -m leaks into the notebook's own namespace -- clobbering the FileArchive objects the notebook created earlier (left = FileArchive ("bridge-l.zip", ...) at cell 207) with the loop's last Image object. Renamed the loop variables to left_img/right_img throughout. - ts = np.loadtxt(left.open("timestamps.dat")) never reliably worked: timestamps.dat isn't a member of bridge-l.zip, it's a separate file in the mvtb-data package (mvtbdata/images/timestamps.dat). left.open() only appeared to succeed when that filename happened to exist in cwd (FileArchive.open() falls back to a plain local read). Switched to mvtb_path_to_datafile("images", "timestamps.dat"). - Processing all 251 frames takes several minutes, so added an optional frame-count CLI arg to visodom.py (all frames if omitted) and the notebook now passes 50. Documented both notebook-visible changes in errata.md (the cv2.error fix needed no notebook change, so it's not listed there per the file's own scope). Co-Authored-By: Claude Sonnet 5 --- RVC3/examples/visodom.py | 44 ++++++++++++++----------- errata.md | 44 +++++++++++++++++++++++++ notebooks/chap14.ipynb | 71 +++++++++++++++++++++++++++++++++++----- 3 files changed, 130 insertions(+), 29 deletions(-) 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/errata.md b/errata.md index 49f55bb..07420ea 100644 --- a/errata.md +++ b/errata.md @@ -493,3 +493,47 @@ 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 +``` + +## §14.8.3 — load `timestamps.dat` via `mvtb_path_to_datafile()`, not `left.open()` + +**Notebook:** `chap14.ipynb` + +**Reason:** `timestamps.dat` isn't a member of `bridge-l.zip` -- it's a +separate file bundled in the `mvtb-data` package, at +`mvtbdata/images/timestamps.dat`. `left.open("timestamps.dat")` only +appeared to work when that exact filename happened to exist in the +current working directory (`FileArchive.open()` falls back to a plain +local file read when the name isn't found inside the archive), which +isn't portable across working directories. `mvtb_path_to_datafile()` +resolves the bundled data file's real location directly. + +**As printed:** +```python +ts = np.loadtxt(left.open("timestamps.dat")); +``` + +**Current toolbox syntax:** +```python +ts = np.loadtxt(mvtb_path_to_datafile("images", "timestamps.dat")); +``` diff --git a/notebooks/chap14.ipynb b/notebooks/chap14.ipynb index a86b71f..f76c10e 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" ] }, { @@ -2286,7 +2339,7 @@ "metadata": {}, "outputs": [], "source": [ - "ts = np.loadtxt(left.open(\"timestamps.dat\"));" + "ts = np.loadtxt(mvtb_path_to_datafile(\"images\", \"timestamps.dat\"));" ] }, { @@ -2326,4 +2379,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} From 938e2b38ccdc114c52e6fead793d2fa9bdf9750a Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 17 Aug 2026 21:23:43 +1000 Subject: [PATCH 06/13] fix: chap14 -- revert timestamps.dat cell, was never actually broken FileArchive.open(name) reads directly from the archive by exact name, bypassing the filter used for iteration entirely -- no cwd fallback, contrary to what the previous commit's errata entry claimed. left.open("timestamps.dat") works correctly once left is the real, un-clobbered FileArchive object, which it now is now that visodom.py no longer reuses the left/right variable names. The namespace clobbering was the entire bug; the book's original line needed no change. Reverting the cell to the as-printed code and dropping the incorrect errata entry. Co-Authored-By: Claude Sonnet 5 --- errata.md | 23 ----------------------- notebooks/chap14.ipynb | 2 +- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/errata.md b/errata.md index 07420ea..5a38ee1 100644 --- a/errata.md +++ b/errata.md @@ -514,26 +514,3 @@ argument to process more frames. ```python %run -m visodom 50 ``` - -## §14.8.3 — load `timestamps.dat` via `mvtb_path_to_datafile()`, not `left.open()` - -**Notebook:** `chap14.ipynb` - -**Reason:** `timestamps.dat` isn't a member of `bridge-l.zip` -- it's a -separate file bundled in the `mvtb-data` package, at -`mvtbdata/images/timestamps.dat`. `left.open("timestamps.dat")` only -appeared to work when that exact filename happened to exist in the -current working directory (`FileArchive.open()` falls back to a plain -local file read when the name isn't found inside the archive), which -isn't portable across working directories. `mvtb_path_to_datafile()` -resolves the bundled data file's real location directly. - -**As printed:** -```python -ts = np.loadtxt(left.open("timestamps.dat")); -``` - -**Current toolbox syntax:** -```python -ts = np.loadtxt(mvtb_path_to_datafile("images", "timestamps.dat")); -``` diff --git a/notebooks/chap14.ipynb b/notebooks/chap14.ipynb index f76c10e..268a7da 100644 --- a/notebooks/chap14.ipynb +++ b/notebooks/chap14.ipynb @@ -2339,7 +2339,7 @@ "metadata": {}, "outputs": [], "source": [ - "ts = np.loadtxt(mvtb_path_to_datafile(\"images\", \"timestamps.dat\"));" + "ts = np.loadtxt(left.open(\"timestamps.dat\"));" ] }, { From c22d87d9e71170524f42c3acdbe6b218aa57986f Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 17 Aug 2026 21:38:54 +1000 Subject: [PATCH 07/13] fix: notebook_runner -- set BDSIM_NO_GRAPHICS=1 for automated runs bdsim now supports BDSIM_NO_GRAPHICS, an unconditional override that disables graphics/animation/movies regardless of what a script's own BDSim(...) constructor call requests -- unlike the BDSIM envariable (lowest priority, loses to explicit constructor kwargs like animation=True and raises "cannot enable animation but disable graphics" instead of silently downgrading). Confirmed this resolves the real slow-render cases from earlier in this branch's history (chap5's braitenberg.py, chap7's jointspace.py, chap9's zerotorque.py/computed-torque-main.py) without the BDSIM=graphics=0 regression that broke scripts passing animation=True explicitly (previously reverted in this same branch). Does not fix chap8's RRMC2.py hang, which reproduces identically with or without this set -- that one was never about render time. Co-Authored-By: Claude Sonnet 5 --- tests/notebook_runner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/notebook_runner.py b/tests/notebook_runner.py index b522bda..3dbb207 100644 --- a/tests/notebook_runner.py +++ b/tests/notebook_runner.py @@ -208,6 +208,7 @@ def main() -> int: args = parser.parse_args() 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 if args.notebooks: paths = [Path(p) for p in args.notebooks] From 911195300fb581c983e572b1d10abdc0805d3cde Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Mon, 17 Aug 2026 21:46:57 +1000 Subject: [PATCH 08/13] fix: chap9 -- feedforward-main.py out.y0 -> out.y bdsim API change bdsim's run() used to store each watch=[...] entry as its own out.y0, out.y1, ... array. A refactor replaced this with a single combined out.y (np.column_stack of every watched signal) plus out.ynames. feedforward-main.py only watches one signal, so out.y is exactly what out.y0 used to be -- confirmed same shape (254, 6). Found the same old .y0/.y1/... pattern in 10 figures/code/chapter* scripts, none of which are exercised by any notebook (not in scope for this notebook-errata pass) -- flagged separately for a follow-up sweep. Co-Authored-By: Claude Sonnet 5 --- RVC3/models/feedforward-main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From bc88ceceb905df45cf5914153a84115b61883292 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 18 Aug 2026 07:50:56 +1000 Subject: [PATCH 09/13] fix: chap15 -- IBVS-main.py out.y2 works via deprecation shim, clock0.x -> clock0.X The AttributeError: y2 is fixed upstream: bdsim's watch=True SCOPE registration silently no-op'd when graphics was disabled, fixed in petercorke/bdsim#75 (promoted watch to GraphicsBlock.__init__) plus a follow-up deprecation shim for the old per-index out.y0/out.y1/... attribute access, so out.y2 still works today with a DeprecationWarning instead of a hard failure. What needed an actual notebook change: out.clock0.x -> out.clock0.X, bdsim's per-clock logged state array uses the uppercase field name. Co-Authored-By: Claude Sonnet 5 --- errata.md | 21 +++++++++++++++++++++ notebooks/chap15.ipynb | 29 +++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/errata.md b/errata.md index 5a38ee1..56bd01d 100644 --- a/errata.md +++ b/errata.md @@ -514,3 +514,24 @@ argument to process more frames. ```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) +``` 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 +} From e628d53de6d0e0f6648446f8f75d46fd3f6c828d Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 18 Aug 2026 10:00:20 +1000 Subject: [PATCH 10/13] fix: chap16 -- IBVS-holonomic/nonholonomic.bd FUNCTION block param rename These .bd files (JSON block-diagram definitions loaded by bdload()) still used bdsim's old FUNCTION block parameter names args/kwargs, renamed to fargs/fkwargs. bdload() passes every "parameters" entry straight through as a constructor kwarg, so the stale names hit Function.__init__()'s **blockargs catch-all and errored with "unexpected keyword arguments ['args']" / "['kwargs']". IBVS-holonomic.bd: 1 FUNCTION block (args). IBVS-nonholonomic.bd: 5 FUNCTION blocks (args), 3 of which also had kwargs. Left FKINE blocks' own args= parameter alone in the other .bd files (IBVS-arm.bd, RRMC2.bd, jointspace.bd) -- that's a different, still-valid parameter on a different block type. Co-Authored-By: Claude Sonnet 5 --- RVC3/models/IBVS-holonomic.bd | 2 +- RVC3/models/IBVS-nonholonomic.bd | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) 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", [] ], [ From a2189bbf3696d487723db97228ff82ddbf33ba95 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 18 Aug 2026 10:09:49 +1000 Subject: [PATCH 11/13] fix: chap16 -- IBVS-quadrotor.bd MultiRotorMixer param rename Same class of stale .bd parameter as the holonomic/nonholonomic fix: this file had maxw/minw, but MultiRotorMixer.__init__() (RTB's blocks/uav.py) accepts wmax/wmin. bdload() passes "parameters" entries straight through as constructor kwargs, so the stale names hit Block.__init__()'s **blockargs catch-all and errored with "unexpected keyword arguments ['maxw', 'minw']". Co-Authored-By: Claude Sonnet 5 --- RVC3/models/IBVS-quadrotor.bd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ], [ From 386cef3bee39e150f29f39e3526dab5334e7d9ab Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Tue, 18 Aug 2026 16:34:17 +1000 Subject: [PATCH 12/13] fix: chap2/app -- tripleangledemo/twistdemo/eigdemo now installed CLI commands RTB moved these from standalone scripts to roboticstoolbox.demo.* modules exposed as installed console commands ([project.scripts] entry points), so %run -m can no longer find them as bare top-level modules ('' is not a valid modulename on sys.path). Invoke them as the installed commands directly instead (!). Co-Authored-By: Claude Sonnet 5 --- errata.md | 58 +++++++++++++++++++++++++++++++++ notebooks/app.ipynb | 53 ++++++++++++++++++++++++++---- notebooks/chap2.ipynb | 76 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 173 insertions(+), 14 deletions(-) diff --git a/errata.md b/errata.md index 56bd01d..b5845a8 100644 --- a/errata.md +++ b/errata.md @@ -535,3 +535,61 @@ plt.plot(out.clock0.t, out.clock0.x) ```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/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/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 +} From 69d687100fb24c6c1ba6051197cc5d7d60bb4793 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Wed, 19 Aug 2026 07:23:28 +1000 Subject: [PATCH 13/13] build: add make test/testall, fold BDSIM_NO_GRAPHICS into pytest path, drop cell numbers from output - Makefile: `make test` runs the fast suite (plain pytest), `make testall` runs the full suite including notebooks/examples (pytest --runall). Kept the fast/slow split -- BDSIM_NO_GRAPHICS only removes bdsim's own render-time slowness, most of --runall's time is unrelated (kernel/cell overhead across 16 notebooks, vision and deep-learning chapters, visodom's bundle adjustment loop). - notebook_runner.run_notebook(): moved the MPLBACKEND/ BDSIM_NO_GRAPHICS os.environ.setdefault() calls here from main(), since tests/test_notebooks.py (the actual pytest --runall path) calls run_notebook() directly and was bypassing main() entirely -- neither var was being set under `make testall`, only when running notebook_runner.py as a CLI script directly. - Dropped cell[N] from both the pytest failure detail and the written report -- not meaningful to a reader, Jupyter doesn't number cells. Report now leads with a concise per-notebook pass/fail summary instead of requiring a scroll through cell-by-cell detail to see which notebooks are clean. Co-Authored-By: Claude Sonnet 5 --- Makefile | 10 +++++++++- tests/notebook_runner.py | 28 +++++++++++++++++++++------- tests/test_notebooks.py | 5 ++++- 3 files changed, 34 insertions(+), 9 deletions(-) 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/tests/notebook_runner.py b/tests/notebook_runner.py index 3dbb207..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,9 +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 - os.environ.setdefault("BDSIM_NO_GRAPHICS", "1") # unconditionally disables bdsim animation/graphics/movies - if args.notebooks: paths = [Path(p) for p in args.notebooks] else: 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