Skip to content

Commit fb5e91b

Browse files
committed
add doctest to user guide + internally generate figures
1 parent 4867159 commit fb5e91b

36 files changed

+592
-319
lines changed

control/tests/flatsys_test.py

Lines changed: 42 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -24,48 +24,6 @@
2424
atol = 1e-4
2525
rtol = 1e-4
2626

27-
# Define the kinematic car system
28-
def vehicle_flat_forward(x, u, params={}):
29-
b = params.get('wheelbase', 3.) # get parameter values
30-
zflag = [np.zeros(3), np.zeros(3)] # list for flag arrays
31-
zflag[0][0] = x[0] # flat outputs
32-
zflag[1][0] = x[1]
33-
zflag[0][1] = u[0] * np.cos(x[2]) # first derivatives
34-
zflag[1][1] = u[0] * np.sin(x[2])
35-
thdot = (u[0]/b) * np.tan(u[1]) # dtheta/dt
36-
zflag[0][2] = -u[0] * thdot * np.sin(x[2]) # second derivatives
37-
zflag[1][2] = u[0] * thdot * np.cos(x[2])
38-
return zflag
39-
40-
def vehicle_flat_reverse(zflag, params={}):
41-
b = params.get('wheelbase', 3.) # get parameter values
42-
x = np.zeros(3); u = np.zeros(2) # vectors to store x, u
43-
x[0] = zflag[0][0] # x position
44-
x[1] = zflag[1][0] # y position
45-
x[2] = np.arctan2(zflag[1][1], zflag[0][1]) # angle
46-
u[0] = zflag[0][1] * np.cos(x[2]) + zflag[1][1] * np.sin(x[2])
47-
thdot_v = zflag[1][2] * np.cos(x[2]) - zflag[0][2] * np.sin(x[2])
48-
u[1] = np.arctan2(thdot_v, u[0]**2 / b)
49-
return x, u
50-
51-
def vehicle_update(t, x, u, params):
52-
b = params.get('wheelbase', 3.) # get parameter values
53-
dx = np.array([
54-
np.cos(x[2]) * u[0],
55-
np.sin(x[2]) * u[0],
56-
(u[0]/b) * np.tan(u[1])
57-
])
58-
return dx
59-
60-
def vehicle_output(t, x, u, params): return x
61-
62-
# Create differentially flat input/output system
63-
vehicle_flat = fs.FlatSystem(
64-
vehicle_flat_forward, vehicle_flat_reverse, vehicle_update,
65-
vehicle_output, inputs=('v', 'delta'), outputs=('x', 'y', 'theta'),
66-
states=('x', 'y', 'theta'), name='vehicle_flat')
67-
68-
6927
class TestFlatSys:
7028
"""Test differential flat systems"""
7129

@@ -100,9 +58,48 @@ def test_double_integrator(self, xf, uf, Tf, basis):
10058
t, y, x = ct.forced_response(sys, T, ud, x1, return_x=True)
10159
np.testing.assert_array_almost_equal(x, xd, decimal=3)
10260

103-
@pytest.fixture(name='vehicle_flat')
104-
def vehicle_flat_fixture(self):
105-
return vehicle_flat
61+
@pytest.fixture
62+
def vehicle_flat(self):
63+
"""Differential flatness for a kinematic car"""
64+
def vehicle_flat_forward(x, u, params={}):
65+
b = params.get('wheelbase', 3.) # get parameter values
66+
zflag = [np.zeros(3), np.zeros(3)] # list for flag arrays
67+
zflag[0][0] = x[0] # flat outputs
68+
zflag[1][0] = x[1]
69+
zflag[0][1] = u[0] * np.cos(x[2]) # first derivatives
70+
zflag[1][1] = u[0] * np.sin(x[2])
71+
thdot = (u[0]/b) * np.tan(u[1]) # dtheta/dt
72+
zflag[0][2] = -u[0] * thdot * np.sin(x[2]) # second derivatives
73+
zflag[1][2] = u[0] * thdot * np.cos(x[2])
74+
return zflag
75+
76+
def vehicle_flat_reverse(zflag, params={}):
77+
b = params.get('wheelbase', 3.) # get parameter values
78+
x = np.zeros(3); u = np.zeros(2) # vectors to store x, u
79+
x[0] = zflag[0][0] # x position
80+
x[1] = zflag[1][0] # y position
81+
x[2] = np.arctan2(zflag[1][1], zflag[0][1]) # angle
82+
u[0] = zflag[0][1] * np.cos(x[2]) + zflag[1][1] * np.sin(x[2])
83+
thdot_v = zflag[1][2] * np.cos(x[2]) - zflag[0][2] * np.sin(x[2])
84+
u[1] = np.arctan2(thdot_v, u[0]**2 / b)
85+
return x, u
86+
87+
def vehicle_update(t, x, u, params):
88+
b = params.get('wheelbase', 3.) # get parameter values
89+
dx = np.array([
90+
np.cos(x[2]) * u[0],
91+
np.sin(x[2]) * u[0],
92+
(u[0]/b) * np.tan(u[1])
93+
])
94+
return dx
95+
96+
def vehicle_output(t, x, u, params): return x
97+
98+
# Create differentially flat input/output system
99+
return fs.FlatSystem(
100+
vehicle_flat_forward, vehicle_flat_reverse, vehicle_update,
101+
vehicle_output, inputs=('v', 'delta'), outputs=('x', 'y', 'theta'),
102+
states=('x', 'y', 'theta'))
106103

107104
@pytest.mark.parametrize("basis", [
108105
fs.PolyFamily(6), fs.PolyFamily(8), fs.BezierFamily(6),
@@ -814,61 +811,3 @@ def test_flatsys_factory_function(self, vehicle_flat):
814811

815812
with pytest.raises(TypeError, match="incorrect number or type"):
816813
flatsys = fs.flatsys(1, 2, 3, 4, 5)
817-
818-
819-
if __name__ == '__main__':
820-
# Generate images for User Guide
821-
import matplotlib.pyplot as plt
822-
823-
#
824-
# Point to point
825-
#
826-
# Define the endpoints of the trajectory
827-
x0 = [0., -2., 0.]; u0 = [10., 0.]
828-
xf = [100., 2., 0.]; uf = [10., 0.]
829-
Tf = 10
830-
831-
# Define a set of basis functions to use for the trajectories
832-
poly = fs.PolyFamily(6)
833-
834-
# Find a trajectory between the initial condition and the final condition
835-
traj = fs.point_to_point(vehicle_flat, Tf, x0, u0, xf, uf, basis=poly)
836-
837-
# Create the trajectory
838-
timepts = np.linspace(0, Tf, 100)
839-
xd, ud = traj.eval(timepts)
840-
resp_p2p = ct.input_output_response(vehicle_flat, timepts, ud, X0=xd[:, 0])
841-
842-
#
843-
# Solve OCP
844-
#
845-
# Define the cost along the trajectory: penalize steering angle
846-
traj_cost = ct.optimal.quadratic_cost(
847-
vehicle_flat, None, np.diag([0.1, 10]), u0=uf)
848-
849-
# Define the terminal cost: penalize distance from the end point
850-
term_cost = ct.optimal.quadratic_cost(
851-
vehicle_flat, np.diag([1e3, 1e3, 1e3]), None, x0=xf)
852-
853-
# Use a straight line as the initial guess
854-
evalpts = np.linspace(0, Tf, 10)
855-
initial_guess = np.array(
856-
[x0[i] + (xf[i] - x0[i]) * evalpts/Tf for i in (0, 1)])
857-
858-
# Solve the optimal control problem, evaluating cost at timepts
859-
bspline = fs.BSplineFamily([0, Tf/2, Tf], 4)
860-
traj = fs.solve_flat_ocp(
861-
vehicle_flat, evalpts, x0, u0, traj_cost,
862-
terminal_cost=term_cost, initial_guess=initial_guess, basis=bspline)
863-
864-
xd, ud = traj.eval(timepts)
865-
resp_ocp = ct.input_output_response(vehicle_flat, timepts, ud, X0=xd[:, 0])
866-
867-
#
868-
# Plot the results
869-
#
870-
cplt = ct.time_response_plot(
871-
ct.combine_time_responses([resp_p2p, resp_ocp]),
872-
overlay_traces=True, trace_labels=['point_to_point', 'solve_ocp'])
873-
874-
plt.savefig('flatsys-steering-compare.png')

control/tests/timeplot_test.py

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -715,58 +715,3 @@ def test_legend_customization():
715715
fig = plt.figure()
716716
cplt = ct.step_response([sys1, sys2]).plot()
717717
cplt.set_plot_title("[List] " + fig._suptitle._text)
718-
719-
#
720-
# Servomechanism example for documentation
721-
#
722-
723-
# Parameter values
724-
servomech_params = {
725-
'J': 100, # Moment of inertia of the motor
726-
'b': 10, # Angular damping of the arm
727-
'k': 1, # Spring constant
728-
'r': 1, # Location of spring contact on arm
729-
'l': 2, # Distance to the read head
730-
'eps': 0.01, # Magnitude of velocity-dependent perturbation
731-
}
732-
733-
# State derivative
734-
def servomech_update(t, x, u, params):
735-
# Extract the configuration and velocity variables from the state vector
736-
theta = x[0] # Angular position of the disk drive arm
737-
thetadot = x[1] # Angular velocity of the disk drive arm
738-
tau = u[0] # Torque applied at the base of the arm
739-
740-
# Get the parameter values
741-
J, b, k, r = map(params.get, ['J', 'b', 'k', 'r'])
742-
743-
# Compute the angular acceleration
744-
dthetadot = 1/J * (
745-
-b * thetadot - k * r * np.sin(theta) + tau)
746-
747-
# Return the state update law
748-
return np.array([thetadot, dthetadot])
749-
750-
# System output (tip radial position + angular velocity)
751-
def servomech_output(t, x, u, params):
752-
l = params['l']
753-
return np.array([l * x[0], x[1]])
754-
755-
# System dynamics
756-
servomech = ct.nlsys(
757-
servomech_update, servomech_output, name='servomech',
758-
params=servomech_params, states=['theta', 'thdot'],
759-
outputs=['y', 'thdot'], inputs=['tau'])
760-
761-
timepts = np.linspace(0, 10)
762-
763-
U1 = np.sin(timepts)
764-
resp1 = ct.input_output_response(servomech, timepts, U1)
765-
766-
U2 = np.cos(2*timepts)
767-
resp2 = ct.input_output_response(servomech, timepts, U2)
768-
769-
plt.figure()
770-
cplt = ct.combine_time_responses(
771-
[resp1, resp2], trace_labels=["Scenario #1", "Scenario #2"]).plot()
772-
plt.savefig('timeplot-servomech-combined.png')

doc/figures/Makefile

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
# RMM, 26 Dec 2024
33

44
# List of figures that need to be created (first figure generated is OK)
5-
FIGS = classes.pdf timeplot-mimo_step-default.png \
6-
freqplot-siso_bode-default.png rlocus-siso_ctime-default.png \
7-
phaseplot-dampedosc-default.png ctrlplot-servomech.png
5+
FIGS = classes.pdf
86

97
# Location of the control package
108
SRCDIR = ../..
@@ -16,18 +14,3 @@ clean:
1614

1715
classes.pdf: classes.fig
1816
fig2dev -Lpdf $< $@
19-
20-
timeplot-mimo_step-default.png: $(SRCDIR)/control/tests/timeplot_test.py
21-
PYTHONPATH=$(SRCDIR) python $<
22-
23-
freqplot-siso_bode-default.png: $(SRCDIR)/control/tests/freqplot_test.py
24-
PYTHONPATH=$(SRCDIR) python $<
25-
26-
rlocus-siso_ctime-default.png: $(SRCDIR)/control/tests/rlocus_test.py
27-
PYTHONPATH=$(SRCDIR) python $<
28-
29-
phaseplot-dampedosc-default.png: $(SRCDIR)/control/tests/phaseplot_test.py
30-
PYTHONPATH=$(SRCDIR) python $<
31-
32-
ctrlplot-servomech.png: $(SRCDIR)/control/tests/ctrlplot_test.py
33-
PYTHONPATH=$(SRCDIR) python $<
1 Byte
Loading

doc/figures/ctrlplot-servomech.png

1 Byte
Loading
18.3 KB
Loading

doc/figures/freqplot-gangof4.png

1 Byte
Loading
1 Byte
Loading
1 Byte
Loading
1 Byte
Loading

0 commit comments

Comments
 (0)