From: <mur...@us...> - 2010-06-07 05:45:51
|
Revision: 23 http://python-control.svn.sourceforge.net/python-control/?rev=23&view=rev Author: murrayrm Date: 2010-06-07 05:45:44 +0000 (Mon, 07 Jun 2010) Log Message: ----------- Working LQR example + lqr() fixes * Fixed bugs in lqr related to matrix multiplication (* vs dot()) * Added PVTOL LQR example (based on AM08 example) * Version 0.3c ready to be released... Modified Paths: -------------- trunk/ChangeLog trunk/examples/pvtol-lqr.py trunk/src/statefbk.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2010-06-06 21:24:07 UTC (rev 22) +++ trunk/ChangeLog 2010-06-07 05:45:44 UTC (rev 23) @@ -1,5 +1,7 @@ 2010-06-06 Richard Murray <murray@sumatra.local> + * examples/pvtol-lqr.py: Added example to test out LQR routines + * src/matlab.py (bode): created a wrapper that allows MATLAB style arguments for bode (eg, bode(sys1, sys2)) Modified: trunk/examples/pvtol-lqr.py =================================================================== --- trunk/examples/pvtol-lqr.py 2010-06-06 21:24:07 UTC (rev 22) +++ trunk/examples/pvtol-lqr.py 2010-06-07 05:45:44 UTC (rev 23) @@ -1,192 +1,195 @@ -% pvtol_lqr.m - LQR design for vectored thrust aircraft -% RMM, 14 Jan 03 +# pvtol_lqr.m - LQR design for vectored thrust aircraft +# RMM, 14 Jan 03 +# +# This file works through an LQR based design problem, using the +# planar vertical takeoff and landing (PVTOL) aircraft example from +# Astrom and Mruray, Chapter 5. It is intended to demonstrate the +# basic functionality of the python-control package. +# -aminit; +from numpy import * # Grab all of the NumPy functions +from matplotlib.pyplot import * # Grab MATLAB plotting functions +from control.matlab import * # MATLAB-like functions -%% -%% System dynamics -%% -%% These are the dynamics for the PVTOL system, written in state space -%% form. -%% +# +# System dynamics +# +# These are the dynamics for the PVTOL system, written in state space +# form. +# -pvtol_params; % System parameters +# System parameters +m = 4; # mass of aircraft +J = 0.0475; # inertia around pitch axis +r = 0.25; # distance to center of force +g = 9.8; # gravitational constant +c = 0.05; # damping factor (estimated) -% System matrices (entire plant: 2 input, 2 output) -xe = [0 0 0 0 0 0]; ue = [0 m*g]; -[A, B, C, D] = pvtol_linearize(xe, ue); +# State space dynamics +xe = [0, 0, 0, 0, 0, 0]; # equilibrium point of interest +ue = [0, m*g]; # (note these are lists, not matrices) -%% -%% Construct inputs and outputs corresponding to steps in xy position -%% -%% The vectors xd and yd correspond to the states that are the desired -%% equilibrium states for the system. The matrices Cx and Cy are the -%% corresponding outputs. -%% -%% The way these vectors are used is to compute the closed loop system -%% dynamics as -%% -%% xdot = Ax + B u => xdot = (A-BK)x + K xd -%% u = -K(x - xd) y = Cx -%% -%% The closed loop dynamics can be simulated using the "step" command, -%% with K*xd as the input vector (assumes that the "input" is unit size, -%% so that xd corresponds to the desired steady state. -%% +# Dynamics matrix (use matrix type so that * works for multiplication) +A = matrix( + [[ 0, 0, 0, 1, 0, 0], + [ 0, 0, 0, 0, 1, 0], + [ 0, 0, 0, 0, 0, 1], + [ 0, 0, (-ue[0]*sin(xe[2]) - ue[1]*cos(xe[2]))/m, -c/m, 0, 0], + [ 0, 0, (ue[0]*cos(xe[2]) - ue[1]*sin(xe[2]))/m, 0, -c/m, 0], + [ 0, 0, 0, 0, 0, 0 ]]) -xd = [1; 0; 0; 0; 0; 0]; Cx = [1 0 0 0 0 0]; -yd = [0; 1; 0; 0; 0; 0]; Cy = [0 1 0 0 0 0]; +# Input matrix +B = matrix( + [[0, 0], [0, 0], [0, 0], + [cos(xe[2])/m, -sin(xe[2])/m], + [sin(xe[2])/m, cos(xe[2])/m], + [r/J, 0]]) -%% -%% LQR design -%% +# Output matrix +C = matrix([[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0]]) +D = matrix([[0, 0], [0, 0]]) -% Start with a diagonal weighting -Qx1 = diag([1, 1, 1, 1, 1, 1]); -Qu1a = diag([1, 1]); -K1a = lqr(A, B, Qx1, Qu1a); +# +# Construct inputs and outputs corresponding to steps in xy position +# +# The vectors xd and yd correspond to the states that are the desired +# equilibrium states for the system. The matrices Cx and Cy are the +# corresponding outputs. +# +# The way these vectors are used is to compute the closed loop system +# dynamics as +# +# xdot = Ax + B u => xdot = (A-BK)x + K xd +# u = -K(x - xd) y = Cx +# +# The closed loop dynamics can be simulated using the "step" command, +# with K*xd as the input vector (assumes that the "input" is unit size, +# so that xd corresponds to the desired steady state. +# -% Close the loop: xdot = Ax + B K (x-xd) -H1a = ss(A-B*K1a, B*K1a*[xd, yd], [Cx; Cy], 0); -[Y, T] = step(H1a, 10); +xd = matrix([[1], [0], [0], [0], [0], [0]]); +yd = matrix([[0], [1], [0], [0], [0], [0]]); -figure(1); clf; subplot(321); - plot(T, Y(:,1, 1), '-', T, Y(:,2, 2), '--', ... - 'Linewidth', AM_data_linewidth); hold on; - plot([0 10], [1 1], 'k-', 'Linewidth', AM_ref_linewidth); hold on; +# +# Extract the relevant dynamics for use with SISO library +# +# The current python-control library only supports SISO transfer +# functions, so we have to modify some parts of the original MATLAB +# code to extract out SISO systems. To do this, we define the 'lat' and +# 'alt' index vectors to consist of the states that are are relevant +# to the lateral (x) and vertical (y) dynamics. +# - amaxis([0, 10, -0.1, 1.4]); - xlabel('time'); ylabel('position'); +# Indices for the parts of the state that we want +lat = (0,2,3,5); +alt = (1,4); - lgh = legend('x', 'y', 'Location', 'southeast'); - legend(lgh, 'boxoff'); - amprint('pvtol-lqrstep1.eps'); +# Decoupled dynamics +Ax = (A[lat, :])[:, lat]; #! not sure why I have to do it this way +Bx = B[lat, 0]; Cx = C[0, lat]; Dx = D[0, 0]; -% Look at different input weightings -Qu1a = diag([1, 1]); K1a = lqr(A, B, Qx1, Qu1a); -H1ax = ss(A-B*K1a,B(:,1)*K1a(1,:)*xd,Cx,0); +Ay = (A[alt, :])[:, alt]; #! not sure why I have to do it this way +By = B[alt, 1]; Cy = C[1, alt]; Dy = D[1, 1]; -Qu1b = 40^2*diag([1, 1]); K1b = lqr(A, B, Qx1, Qu1b); -H1bx = ss(A-B*K1b,B(:,1)*K1b(1,:)*xd,Cx,0); +# Label the plot +clf(); +suptitle("LQR controllers for vectored thrust aircraft (pvtol-lqr)") -Qu1c = 200^2*diag([1, 1]); K1c = lqr(A, B, Qx1, Qu1c); -H1cx = ss(A-B*K1c,B(:,1)*K1c(1,:)*xd,Cx,0); +# +# LQR design +# -[Y1, T1] = step(H1ax, 10); -[Y2, T2] = step(H1bx, 10); -[Y3, T3] = step(H1cx, 10); +# Start with a diagonal weighting +Qx1 = diag([1, 1, 1, 1, 1, 1]); +Qu1a = diag([1, 1]); +(K, X, E) = lqr(A, B, Qx1, Qu1a); K1a = matrix(K); -figure(2); clf; subplot(321); - plot(T1, Y1, 'b-', 'Linewidth', AM_data_linewidth); hold on; - plot(T2, Y2, 'b-', 'Linewidth', AM_data_linewidth); hold on; - plot(T3, Y3, 'b-', 'Linewidth', AM_data_linewidth); hold on; - plot([0 10], [1 1], 'k-', 'Linewidth', AM_ref_linewidth); hold on; +# Close the loop: xdot = Ax - B K (x-xd) +# Note: python-control requires we do this 1 input at a time +# H1a = ss(A-B*K1a, B*K1a*concatenate((xd, yd), axis=1), C, D); +# (T, Y) = step(H1a, T=linspace(0,10,100)); - amaxis([0, 10, -0.1, 1.4]); - xlabel('time'); ylabel('position'); +# Step response for the first input +H1ax = ss(Ax - Bx*K1a[0,lat], Bx*K1a[0,lat]*xd[lat,:], Cx, Dx); +(Tx, Yx) = step(H1ax, T=linspace(0,10,100)); +print Tx.shape +print Yx.shape - arcarrow([1.3 0.8], [5 0.45], -6); - text(5.3, 0.4, 'rho'); +# Step response for the second input +H1ay = ss(Ay - By*K1a[1,alt], By*K1a[1,alt]*yd[alt,:], Cy, Dy); +(Ty, Yy) = step(H1ay, T=linspace(0,10,100)); - amprint('pvtol-lqrstep2.eps'); +subplot(221); title("Identity weights") +# plot(T, Y[:,1, 1], '-', T, Y[:,2, 2], '--'); hold(True); +plot(Tx, Yx[0,:].T, '-', Ty, Yy[0,:].T, '--'); hold(True); +plot([0, 10], [1, 1], 'k-'); hold(True); -% Output weighting - change Qx to use outputs -Qx2 = [Cx; Cy]' * [Cx; Cy]; -Qu2 = 0.1 * diag([1, 1]); -K2 = lqr(A, B, Qx2, Qu2); +axis([0, 10, -0.1, 1.4]); +ylabel('position'); +legend(('x', 'y'), loc='lower right'); -H2x = ss(A-B*K2,B(:,1)*K2(1,:)*xd,Cx,0); -H2y = ss(A-B*K2,B(:,2)*K2(2,:)*yd,Cy,0); +# Look at different input weightings +Qu1a = diag([1, 1]); (K1a, X, E) = lqr(A, B, Qx1, Qu1a); +H1ax = ss(Ax - Bx*K1a[0,lat], Bx*K1a[0,lat]*xd[lat,:], Cx, Dx); -figure(3); step(H2x, H2y, 10); -legend('x', 'y'); +Qu1b = (40**2)*diag([1, 1]); (K1b, X, E) = lqr(A, B, Qx1, Qu1b); +H1bx = ss(Ax - Bx*K1b[0,lat], Bx*K1b[0,lat]*xd[lat,:],Cx, Dx); -%% -%% Physically motivated weighting -%% -%% Shoot for 1 cm error in x, 10 cm error in y. Try to keep the angle -%% less than 5 degrees in making the adjustments. Penalize side forces -%% due to loss in efficiency. -%% +Qu1c = (200**2)*diag([1, 1]); (K1c, X, E) = lqr(A, B, Qx1, Qu1c); +H1cx = ss(Ax - Bx*K1c[0,lat], Bx*K1c[0,lat]*xd[lat,:],Cx, Dx); -Qx3 = diag([100, 10, 2*pi/5, 0, 0, 0]); -Qu3 = 0.1 * diag([1, 10]); -K3 = lqr(A, B, Qx3, Qu3); +[T1, Y1] = step(H1ax, T=linspace(0,10,100)); +[T2, Y2] = step(H1bx, T=linspace(0,10,100)); +[T3, Y3] = step(H1cx, T=linspace(0,10,100)); -H3x = ss(A-B*K3,B(:,1)*K3(1,:)*xd,Cx,0); -H3y = ss(A-B*K3,B(:,2)*K3(2,:)*yd,Cy,0); -figure(4); clf; subplot(221); -step(H3x, H3y, 10); -legend('x', 'y'); +subplot(222); title("Effect of input weights") +plot(T1, Y1[0,:].T, 'b-'); hold(True); +plot(T2, Y2[0,:].T, 'b-'); hold(True); +plot(T3, Y3[0,:].T, 'b-'); hold(True); +plot([0 ,10], [1, 1], 'k-'); hold(True); -%% -%% Velocity control -%% -%% In this example, we modify the system so that we control the -%% velocity of the system in the x direction. We ignore the -%% dynamics in the vertical (y) direction. These dynamics demonstrate -%% the role of the feedforward system since the equilibrium point -%% corresponding to vd neq 0 requires a nonzero input. -%% -%% For this example, we use a control law u = -K(x-xd) + ud and convert -%% this to the form u = -K x + N r, where r is the reference input and -%% N is computed as described in class. -%% +axis([0, 10, -0.1, 1.4]); -% Extract system dynamics: theta, xdot, thdot -Av = A([3 4 6], [3 4 6]); -Bv = B([3 4 6], 1); -Cv = [0 1 0]; % choose vx as output -Dv = 0; +# arcarrow([1.3, 0.8], [5, 0.45], -6); +text(5.3, 0.4, 'rho'); -% Design the feedback term using LQR -Qxv = diag([2*pi/5, 10, 0]); -Quv = 0.1; -Kv = lqr(Av, Bv, Qxv, Quv); +# Output weighting - change Qx to use outputs +Qx2 = C.T * C; +Qu2 = 0.1 * diag([1, 1]); +(K, X, E) = lqr(A, B, Qx2, Qu2); K2 = matrix(K) -% Design the feedforward term by solve for eq pt in terms of reference r -T = [Av Bv; Cv Dv]; % system matrix -Nxu = T \ [0; 0; 0; 1]; % compute [Nx; Nu] -Nx = Nxu(1:3); Nu = Nxu(4); % extract Nx and Nu -N = Nu + Kv*Nx; % compute feedforward term +H2x = ss(Ax - Bx*K2[0,lat], Bx*K2[0,lat]*xd[lat,:], Cx, Dx); +H2y = ss(Ay - By*K2[1,alt], By*K2[1,alt]*yd[alt,:], Cy, Dy); -%% -%% Design #1: no feedforward input, ud -%% +subplot(223); title("Output weighting") +[T2x, Y2x] = step(H2x, T=linspace(0,10,100)); +[T2y, Y2y] = step(H2y, T=linspace(0,10,100)); +plot(T2x, Y2x[0,:].T, T2y, Y2y[0,:].T) +ylabel('position'); +xlabel('time'); ylabel('position'); +legend(('x', 'y'), loc='lower right'); -Nv1 = [0; 1; 0]; -Hv1 = ss(Av-Bv*Kv, Bv*Kv*Nx, Cv, 0); -step(Hv1, 10); +# +# Physically motivated weighting +# +# Shoot for 1 cm error in x, 10 cm error in y. Try to keep the angle +# less than 5 degrees in making the adjustments. Penalize side forces +# due to loss in efficiency. +# -%% -%% Design #2: compute feedforward gain corresponding to equilibrium point -%% +Qx3 = diag([100, 10, 2*pi/5, 0, 0, 0]); +Qu3 = 0.1 * diag([1, 10]); +(K, X, E) = lqr(A, B, Qx3, Qu3); K3 = matrix(K); -Hv2 = ss(Av-Bv*Kv, Bv*N, Cv, 0); -step(Hv2, 10); +H3x = ss(Ax - Bx*K3[0,lat], Bx*K3[0,lat]*xd[lat,:], Cx, Dx); +H3y = ss(Ay - By*K3[1,alt], By*K3[1,alt]*yd[alt,:], Cy, Dy); +subplot(224) +# step(H3x, H3y, 10); +[T3x, Y3x] = step(H3x, T=linspace(0,10,100)); +[T3y, Y3y] = step(H3y, T=linspace(0,10,100)); +plot(T3x, Y3x[0,:].T, T3y, Y3y[0,:].T) +title("Physically motivated weights") +xlabel('time'); +legend(('x', 'y'), loc='lower right'); -%% -%% Design #3: integral action -%% -%% Add a new state to the system that is given by xidot = v - vd. We -%% construct the control law by computing an LQR gain for the augmented -%% system. -%% - -Ai = [Av, [0; 0; 0]; [Cv, 0]]; -Bi = [Bv; 0]; -Ci = [Cv, 0]; -Di = Dv; - -% Design the feedback term, including weight on integrator error -Qxi = diag([2*pi/5, 10, 0, 10]); -Qui = 0.1; -Ki = lqr(Ai, Bi, Qxi, Qui); - -% Desired state (augmented) -xid = [0; 1; 0; 0]; - -% Construct the closed loop system (including integrator) -Hi = ss(Ai-Bi*Ki,Bi*Ki*xid - [0; 0; 0; Ci*xid],Ci,0); -step(Hi, 10); - +show() Modified: trunk/src/statefbk.py =================================================================== --- trunk/src/statefbk.py 2010-06-06 21:24:07 UTC (rev 22) +++ trunk/src/statefbk.py 2010-06-07 05:45:44 UTC (rev 23) @@ -41,6 +41,7 @@ # External packages and modules import numpy as np +import ctrlutil from control.exception import * # Pole placement @@ -139,8 +140,7 @@ if (len(args) < 4): raise ControlArgument("not enough input arguments") - elif (getattr(args[0], 'A', None) and - getattr(args[0], 'B', None)): + elif (ctrlutil.issys(args[0])): # We were passed a system as the first argument; extract A and B #! TODO: really just need to check for A and B attributes A = np.array(args[0].A, ndmin=2, dtype=float); @@ -179,7 +179,7 @@ X,rcond,w,S,U = sb02md(nstates, A_b, G, Q_b, 'C') # Now compute the return value - K = np.linalg.inv(R) * (np.transpose(B) * X + np.transpose(N)); + K = np.dot(np.linalg.inv(R), (np.dot(B.T, X) + N.T)); S = X; E = w[0:nstates]; This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2010-06-11 04:51:01
|
Revision: 24 http://python-control.svn.sourceforge.net/python-control/?rev=24&view=rev Author: murrayrm Date: 2010-06-11 04:50:54 +0000 (Fri, 11 Jun 2010) Log Message: ----------- minor code cleanup Modified Paths: -------------- trunk/examples/pvtol-lqr.py trunk/examples/pvtol-nested.py trunk/src/freqplot.py Modified: trunk/examples/pvtol-lqr.py =================================================================== --- trunk/examples/pvtol-lqr.py 2010-06-07 05:45:44 UTC (rev 23) +++ trunk/examples/pvtol-lqr.py 2010-06-11 04:50:54 UTC (rev 24) @@ -112,8 +112,6 @@ # Step response for the first input H1ax = ss(Ax - Bx*K1a[0,lat], Bx*K1a[0,lat]*xd[lat,:], Cx, Dx); (Tx, Yx) = step(H1ax, T=linspace(0,10,100)); -print Tx.shape -print Yx.shape # Step response for the second input H1ay = ss(Ay - By*K1a[1,alt], By*K1a[1,alt]*yd[alt,:], Cy, Dy); Modified: trunk/examples/pvtol-nested.py =================================================================== --- trunk/examples/pvtol-nested.py 2010-06-07 05:45:44 UTC (rev 23) +++ trunk/examples/pvtol-nested.py 2010-06-11 04:50:54 UTC (rev 24) @@ -144,5 +144,5 @@ # Gang of Four figure(11); clf(); -gangof4(Hi*Po, Co, linspace(-2, 3)); +gangof4(Hi*Po, Co); Modified: trunk/src/freqplot.py =================================================================== --- trunk/src/freqplot.py 2010-06-07 05:45:44 UTC (rev 23) +++ trunk/src/freqplot.py 2010-06-11 04:50:54 UTC (rev 24) @@ -52,7 +52,7 @@ Usage ===== - (magh, phaseh) = bode(sys, omega=None, dB=False, Hz=False) + (magh, phaseh) = bode(syslist, omega=None, dB=False, Hz=False) Plots a Bode plot for the system over a (optional) frequency range. This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2010-06-17 21:29:29
|
Revision: 26 http://python-control.svn.sourceforge.net/python-control/?rev=26&view=rev Author: murrayrm Date: 2010-06-17 21:29:23 +0000 (Thu, 17 Jun 2010) Log Message: ----------- added Ryan Krauss's controls.py module for easy access (and version control) Added Paths: ----------- trunk/external/ trunk/external/controls.py Added: trunk/external/controls.py =================================================================== --- trunk/external/controls.py (rev 0) +++ trunk/external/controls.py 2010-06-17 21:29:23 UTC (rev 26) @@ -0,0 +1,1386 @@ +# controls.py - Ryan Krauss's control module +# $Id$ + +"""This module is for analyzing linear, time-invariant dynamic systems +and feedback control systems using the Laplace transform. The heart +of the module is the TransferFunction class, which represents a +transfer function as a ratio of numerator and denominator polynomials +in s. TransferFunction is derived from scipy.signal.lti.""" + +import glob, pdb +from math import atan2, log10 + +from scipy import * +from scipy.linalg import inv as inverse +from scipy.optimize import newton, fmin, fminbound +from scipy.io import read_array, save, loadmat, write_array +from scipy import signal + +from IPython.Debugger import Pdb + +import sys, os, copy, time + +from matplotlib.ticker import LogFormatterMathtext + +version = '1.1.0' + +class MyFormatter(LogFormatterMathtext): + def __call__(self, x, pos=None): + if pos==0: return '' # pos=0 is the first tick + else: return LogFormatterMathtext.__call__(self, x, pos) + + +def shift(vectin, new): + N = len(vectin)-1 + for n in range(N,0,-1): + vectin[n]=vectin[n-1] + vectin[0]=new + return vectin + +def myeq(p1,p2): + """Test the equality of the of two polynomials based on + coeffiecents.""" + if hasattr(p1, 'coeffs') and hasattr(p2, 'coeffs'): + c1=p1.coeffs + c2=p2.coeffs + else: + return False + if len(c1)!=len(c2): + return False + else: + testvect=c1==c2 + if hasattr(testvect,'all'): + return testvect.all() + else: + return testvect + +def build_fit_matrix(output_vect, input_vect, numorder, denorder): + """Build the [A] matrix used in least squares curve fitting + according to + + output_vect = [A]c + + as described in fit_discrete_response.""" + A = zeros((len(output_vect),numorder+denorder+1))#the +1 accounts + #for the fact that both the numerator and the denominator + #have zero-order terms (which would give +2), but the + #zero order denominator term is actually not used in the fit + #(that is the output vector) + curin = input_vect + A[:,0] = curin + for n in range(1, numorder+1): + curin = r_[[0.0], curin[0:-1]]#prepend a 0 to curin and drop its + #last element + A[:,n] = curin + curout = -output_vect#this is the first output column, but it not + #actually used + firstden = numorder+1 + for n in range(0, denorder): + curout = r_[[0.0], curout[0:-1]] + A[:,firstden+n] = curout + return A + + +def fit_discrete_response(output_vect, input_vect, numorder, denorder): + """Find the coefficients of a digital transfer function that give + the best fit to output_vect in a least squares sense. output_vect + is the output of the system and input_vect is the input. The + input and output vectors are shifted backward in time a maximum of + numorder and denorder steps respectively. Each shifted vector + becomes a column in the matrix for the least squares curve fit of + the form + + output_vect = [A]c + + where [A] is the matrix whose columns are shifted versions of + input_vect and output_vect and c is composed of the numerator and + denominator coefficients of the transfer function. numorder and + denorder are the highest power of z in the numerator or + denominator respectively. + + In essence, the approach is to find the coefficients that best fit + related the input_vect and output_vect according to the difference + equation + + y(k) = b_0 x(k) + b_1 x(k-1) + b_2 x(k-2) + ... + b_m x(k-m) + - a_1 y(k-1) - a_2 y(k-2) - ... - a_n y(k-n) + + where x = input_vect, y = output_vect, m = numorder, and + n = denorder. The unknown coefficient vector is then + + c = [b_0, b_1, b_2, ... , b_m, a_1, a_2, ..., a_n] + + Note that a_0 is forced to be 1. + + The matrix [A] is then composed of [A] = [X(k) X(k-1) X(k-2) + ... Y(k-1) Y(k-2) ...] where X(k-2) represents the input_vect + shifted 2 elements and Y(k-2) represents the output_vect shifted + two elements.""" + A = build_fit_matrix(output_vect, input_vect, numorder, denorder) + fitres = linalg.lstsq(A, output_vect) + x = fitres[0] + numz = x[0:numorder+1] + denz = x[numorder+1:] + denz = r_[[1.0],denz] + return numz, denz + +def prependzeros(num, den): + nd = len(den) + nn = len(num) + if nn < nd: + zvect = zeros(nd-nn) + numout = r_[zvect, num] + else: + numout = num + return numout, den + +def in_with_tol(elem, searchlist, rtol=1e-5, atol=1e-10): + """Determine whether or not elem+/-tol matches an element of + searchlist.""" + for n, item in enumerate(searchlist): + if allclose(item, elem, rtol=rtol, atol=atol): + return n + return -1 + + + +def PolyToLatex(polyin, var='s', fmt='%0.5g', eps=1e-12): + N = polyin.order + clist = polyin.coeffs + outstr = '' + for i, c in enumerate(clist): + curexp = N-i + curcoeff = fmt%c + if curexp > 0: + if curexp == 1: + curs = var + else: + curs = var+'^%i'%curexp + #Handle coeffs of +/- 1 in a special way: + if 1-eps < c < 1+eps: + curcoeff = '' + elif -1-eps < c < -1+eps: + curcoeff = '-' + else: + curs='' + curstr = curcoeff+curs + if c > 0 and outstr: + curcoeff = '+'+curcoeff + if abs(c) > eps: + outstr+=curcoeff+curs + return outstr + + +def polyfactor(num, den, prepend=True, rtol=1e-5, atol=1e-10): + """Factor out any common roots from the polynomials represented by + the vectors num and den and return new coefficient vectors with + any common roots cancelled. + + Because poly1d does not think in terms of z^-1, z^-2, etc. it may + be necessary to add zeros to the beginning of the numpoly coeffs + to represent multiplying through be z^-n where n is the order of + the denominator. If prependzeros is Trus, the numerator and + denominator coefficient vectors will have the same length.""" + numpoly = poly1d(num) + denpoly = poly1d(den) + nroots = roots(numpoly).tolist() + droots = roots(denpoly).tolist() + n = 0 + while n < len(nroots): + curn = nroots[n] + ind = in_with_tol(curn, droots, rtol=rtol, atol=atol) + if ind > -1: + nroots.pop(n) + droots.pop(ind) + #numpoly, rn = polydiv(numpoly, poly(curn)) + #denpoly, rd = polydiv(denpoly, poly(curn)) + else: + n += 1 + numpoly = poly(nroots) + denpoly = poly(droots) + nvect = numpoly + dvect = denpoly + if prepend: + nout, dout = prependzeros(nvect, dvect) + else: + nout = nvect + dout = dvect + return nout, dout + + +def polysubstitute(polyin, numsub, densub): + """Substitute one polynomial into another to support Tustin and + other c2d algorithms of a similar approach. The idea is to make + it easy to substitute + + a z-1 + s = - ----- + T z+1 + + or other forms involving ratios of polynomials for s in a + polynomial of s such as the numerator or denominator of a transfer + function. + + For the tustin example above, numsub=a*(z-1) and densub=T*(z+1), + where numsub and densub are scipy.poly1d instances. + + Note that this approach seems to have substantial floating point + problems.""" + mys = TransferFunction(numsub, densub) + out = 0.0 + no = polyin.order + for n, coeff in enumerate(polyin.coeffs): + curterm = coeff*mys**(no-n) + out = out+curterm + return out + + +def tustin_sub(polyin, T, a=2.0): + numsub = a*poly1d([1.0,-1.0]) + densub = T*poly1d([1.0,1.0]) + out = polysubstitute(polyin, numsub, densub) + out.myvar = 'z' + return out + + +def create_swept_sine_input(maxt, dt, maxf, minf=0.0, deadtime=2.0): + t = arange(0, maxt, dt) + u = sweptsine(t, minf=minf, maxf=maxf) + if deadtime: + deadt = arange(0,deadtime, dt) + zv = zeros_like(deadt) + u = r_[zv, u, zv] + return u + +def create_swept_sine_t(maxt, dt, deadtime=2.0): + t = arange(0, maxt, dt) + if deadtime: + deadt = arange(0,deadtime, dt) + t = t+max(deadt)+dt + tpost = deadt+max(t)+dt + return r_[deadt, t, tpost] + else: + return t + +def ADC(vectin, bits=9, vmax=2.5, vmin=-2.5): + """Simulate the sampling portion of an analog-to-digital + conversion by outputing an integer number of counts associate with + each voltage in vectin.""" + dv = (vmax-vmin)/2**bits + vect2 = clip(vectin, vmin, vmax) + counts = vect2/dv + return counts.astype(int) + + +def CountsToFloat(counts, bits=9, vmax=2.5, vmin=-2.5): + """Convert the integer output of ADC to a floating point number by + mulitplying by dv.""" + dv = (vmax-vmin)/2**bits + return dv*counts + + +def epslist(listin, eps=1.0e-12): + """Make a copy of listin and then check each element of the copy + to see if its absolute value is greater than eps. Set to zero all + elements in the copied list whose absolute values are less than + eps. Return the copied list.""" + listout = copy.deepcopy(listin) + for i in range(len(listout)): + if abs(listout[i])<eps: + listout[i] = 0.0 + return listout + + +def _PlotMatrixvsF(freqvect,matin,linetype='',linewidth=None, semilogx=True, allsolid=False, axis=None): + mykwargs={} + usepylab = False + if axis is None: + import pylab + axis = pylab.gca() + usepylab = True + if len(shape(matin))==1: + myargs=[freqvect,matin] + if linetype: + myargs.append(linetype) + else: + mykwargs.update(_getlinetype(axis)) + if linewidth: + mykwargs['linewidth']=linewidth + if semilogx: + curline,=axis.semilogx(*myargs,**mykwargs) + else: + curline,=axis.plot(*myargs,**mykwargs) + mylines=[curline] +# _inccount() + else: + mylines=[] + for q in range(shape(matin)[1]): + myargs=[freqvect,matin[:,q]] + if linetype: + myargs.append(linetype) + else: + mykwargs.update(_getlinetype(axis)) + if linewidth: + mykwargs['linewidth']=linewidth + if semilogx: + curline,=axis.semilogx(*myargs,**mykwargs) + else: + curline,=axis.plot(*myargs,**mykwargs) + mylines.append(curline) +# _inccount() + return mylines + + +def _PlotMag(freqvect, bodein, linetype='-', linewidth=0, axis=None): + if callable(bodein.dBmag): + myvect=bodein.dBmag() + else: + myvect=bodein.dBmag + return _PlotMatrixvsF(freqvect, myvect, linetype=linetype, linewidth=linewidth, axis=axis) + + +def _PlotPhase(freqvect, bodein, linetype='-', linewidth=0, axis=None): + return _PlotMatrixvsF(freqvect,bodein.phase,linetype=linetype,linewidth=linewidth, axis=axis) + + +def _k_poles(TF,poleloc): + L = TF.num(poleloc)/TF.den(poleloc) + k = 1.0/abs(L) + poles = TF._RLFindRoots([k]) + poles = TF._RLSortRoots(poles) + return k,poles + +def _checkpoles(poleloc,pnew): + evect = abs(poleloc-array(pnew)) + ind = evect.argmin() + pout = pnew[ind] + return pout + + +def _realizable(num, den): + realizable = False + if not isscalar(den): + if isscalar(num): + realizable = True + elif len(den) >= len(num): + realizable = True + return realizable + + +def shape_u(uvect, slope): + u_shaped = zeros_like(uvect) + u_shaped[0] = uvect[0] + + N = len(uvect) + + for n in range(1, N): + diff = uvect[n] - u_shaped[n-1] + if diff > slope: + u_shaped[n] = u_shaped[n-1] + slope + elif diff < -1*slope: + u_shaped[n] = u_shaped[n-1] - slope + else: + u_shaped[n] = uvect[n] + return u_shaped + + +class TransferFunction(signal.lti): + def __setattr__(self, attr, val): + realizable = False + if hasattr(self, 'den') and hasattr(self, 'num'): + realizable = _realizable(self.num, self.den) + if realizable: + signal.lti.__setattr__(self, attr, val) + else: + self.__dict__[attr] = val + + + def __init__(self, num, den, dt=0.01, maxt=5.0, myvar='s'): + """num and den are either scalar constants or lists that are + passed to scipy.poly1d to create a list of coefficients.""" + #print('in TransferFunction.__init__, dt=%s' % dt) + if _realizable(num, den): + signal.lti.__init__(self, num, den) + self.num = poly1d(num) + self.den = poly1d(den) + self.dt = dt + self.myvar = myvar + self.maxt = maxt + + + def __repr__(self, labelstr='controls.TransferFunction'): + nstr=str(self.num)#.strip() + dstr=str(self.den)#.strip() + nstr=nstr.replace('x',self.myvar) + dstr=dstr.replace('x',self.myvar) + n=len(dstr) + m=len(nstr) + shift=(n-m)/2*' ' + nstr=nstr.replace('\n','\n'+shift) + tempstr=labelstr+'\n'+shift+nstr+'\n'+'-'*n+'\n '+dstr + return tempstr + + + def __call__(self,s,optargs=()): + return self.num(s)/self.den(s) + + + def __add__(self,other): + if hasattr(other,'num') and hasattr(other,'den'): + if len(self.den.coeffs)==len(other.den.coeffs) and \ + (self.den.coeffs==other.den.coeffs).all(): + return TransferFunction(self.num+other.num,self.den) + else: + return TransferFunction(self.num*other.den+other.num*self.den,self.den*other.den) + elif isinstance(other, int) or isinstance(other, float): + return TransferFunction(other*self.den+self.num,self.den) + else: + raise ValueError, 'do not know how to add TransferFunction and '+str(other) +' which is of type '+str(type(other)) + + def __radd__(self,other): + return self.__add__(other) + + + def __mul__(self,other): + if isinstance(other, Digital_P_Control): + return self.__class__(other.kp*self.num, self.den) + elif hasattr(other,'num') and hasattr(other,'den'): + if myeq(self.num,other.den) and myeq(self.den,other.num): + return 1 + elif myeq(self.num,other.den): + return self.__class__(other.num,self.den) + elif myeq(self.den,other.num): + return self.__class__(self.num,other.den) + else: + gain = self.gain*other.gain + new_num, new_den = polyfactor(self.num*other.num, \ + self.den*other.den) + newtf = self.__class__(new_num*gain, new_den) + return newtf + elif isinstance(other, int) or isinstance(other, float): + return self.__class__(other*self.num,self.den) + + + def __pow__(self, expon): + """Basically, go self*self*self as many times as necessary. I + haven't thought about negative exponents. I don't think this + would be hard, you would just need to keep dividing by self + until you got the right answer.""" + assert expon >= 0, 'TransferFunction.__pow__ does not yet support negative exponents.' + out = 1.0 + for n in range(expon): + out *= self + return out + + + def __rmul__(self,other): + return self.__mul__(other) + + + def __div__(self,other): + if hasattr(other,'num') and hasattr(other,'den'): + if myeq(self.den,other.den): + return TransferFunction(self.num,other.num) + else: + return TransferFunction(self.num*other.den,self.den*other.num) + elif isinstance(other, int) or isinstance(other, float): + return TransferFunction(self.num,other*self.den) + + + def __rdiv__(self, other): + print('calling TransferFunction.__rdiv__') + return self.__div__(other) + + + def __truediv__(self,other): + return self.__div__(other) + + + def _get_set_dt(self, dt=None): + if dt is not None: + self.dt = float(dt) + return self.dt + + + def ToLatex(self, eps=1e-12, fmt='%0.5g', ds=True): + mynum = self.num + myden = self.den + npart = PolyToLatex(mynum) + dpart = PolyToLatex(myden) + outstr = '\\frac{'+npart+'}{'+dpart+'}' + if ds: + outstr = '\\displaystyle '+outstr + return outstr + + + def RootLocus(self, kvect, fig=None, fignum=1, \ + clear=True, xlim=None, ylim=None, plotstr='-'): + """Calculate the root locus by finding the roots of 1+k*TF(s) + where TF is self.num(s)/self.den(s) and each k is an element + of kvect.""" + if fig is None: + import pylab + fig = pylab.figure(fignum) + if clear: + fig.clf() + ax = fig.add_subplot(111) + mymat = self._RLFindRoots(kvect) + mymat = self._RLSortRoots(mymat) + #plot open loop poles + poles = array(self.den.r) + ax.plot(real(poles), imag(poles), 'x') + #plot open loop zeros + zeros = array(self.num.r) + if zeros.any(): + ax.plot(real(zeros), imag(zeros), 'o') + for col in mymat.T: + ax.plot(real(col), imag(col), plotstr) + if xlim: + ax.set_xlim(xlim) + if ylim: + ax.set_ylim(ylim) + ax.set_xlabel('Real') + ax.set_ylabel('Imaginary') + return mymat + + + def _RLFindRoots(self, kvect): + """Find the roots for the root locus.""" + roots = [] + for k in kvect: + curpoly = self.den+k*self.num + curroots = curpoly.r + curroots.sort() + roots.append(curroots) + mymat = row_stack(roots) + return mymat + + + def _RLSortRoots(self, mymat): + """Sort the roots from self._RLFindRoots, so that the root + locus doesn't show weird pseudo-branches as roots jump from + one branch to another.""" + sorted = zeros_like(mymat) + for n, row in enumerate(mymat): + if n==0: + sorted[n,:] = row + else: + #sort the current row by finding the element with the + #smallest absolute distance to each root in the + #previous row + available = range(len(prevrow)) + for elem in row: + evect = elem-prevrow[available] + ind1 = abs(evect).argmin() + ind = available.pop(ind1) + sorted[n,ind] = elem + prevrow = sorted[n,:] + return sorted + + + def opt(self, kguess): + pnew = self._RLFindRoots(kguess) + pnew = self._RLSortRoots(pnew)[0] + if len(pnew)>1: + pnew = _checkpoles(self.poleloc,pnew) + e = abs(pnew-self.poleloc)**2 + return sum(e) + + + def rlocfind(self, poleloc): + self.poleloc = poleloc + kinit,pinit = _k_poles(self,poleloc) + k = optimize.fmin(self.opt,[kinit])[0] + poles = self._RLFindRoots([k]) + poles = self._RLSortRoots(poles) + return k, poles + + + def PlotTimeResp(self, u, t, fig, clear=True, label='model', mysub=111): + ax = fig.add_subplot(mysub) + if clear: + ax.cla() + try: + y = self.lsim(u, t) + except: + y = self.lsim2(u, t) + ax.plot(t, y, label=label) + return ax + + +## def BodePlot(self, f, fig, clear=False): +## mtf = self.FreqResp( +## ax1 = fig.axes[0] +## ax1.semilogx(modelf,20*log10(abs(mtf))) +## mphase = angle(mtf, deg=1) +## ax2 = fig.axes[1] +## ax2.semilogx(modelf, mphase) + + + def SimpleFactor(self): + mynum=self.num + myden=self.den + dsf=myden[myden.order] + nsf=mynum[mynum.order] + sden=myden/dsf + snum=mynum/nsf + poles=sden.r + residues=zeros(shape(sden.r),'D') + factors=[] + for x,p in enumerate(poles): + polearray=poles.copy() + polelist=polearray.tolist() + mypole=polelist.pop(x) + tempden=1.0 + for cp in polelist: + tempden=tempden*(poly1d([1,-cp])) + tempTF=TransferFunction(snum,tempden) + curres=tempTF(mypole) + residues[x]=curres + curTF=TransferFunction(curres,poly1d([1,-mypole])) + factors.append(curTF) + return factors,nsf,dsf + + def factor_constant(self, const): + """Divide numerator and denominator coefficients by const""" + self.num = self.num/const + self.den = self.den/const + + def lsim(self, u, t, interp=0, returnall=False, X0=None): + """Find the response of the TransferFunction to the input u + with time vector t. Uses signal.lsim. + + return y the response of the system.""" + if returnall:#most users will just want the system output y, + #but some will need the (t, y, x) tuple that + #signal.lsim returns + return signal.lsim(self, u, t, interp=interp, X0=X0) + else: + return signal.lsim(self, u, t, interp=interp, X0=X0)[1] + + def lsim2(self, u, t, returnall=False, X0=None): + #tempsys=signal.lti(self.num,self.den) + if returnall: + return signal.lsim2(self, u, t, X0=X0) + else: + return signal.lsim2(self, u, t, X0=X0)[1] + + + def residue(self, tol=1e-3, verbose=0): + """from scipy.signal.residue: + + Compute residues/partial-fraction expansion of b(s) / a(s). + + If M = len(b) and N = len(a) + + b(s) b[0] s**(M-1) + b[1] s**(M-2) + ... + b[M-1] + H(s) = ------ = ---------------------------------------------- + a(s) a[0] s**(N-1) + a[1] s**(N-2) + ... + a[N-1] + + r[0] r[1] r[-1] + = -------- + -------- + ... + --------- + k(s) + (s-p[0]) (s-p[1]) (s-p[-1]) + + If there are any repeated roots (closer than tol), then the + partial fraction expansion has terms like + + r[i] r[i+1] r[i+n-1] + -------- + ----------- + ... + ----------- + (s-p[i]) (s-p[i])**2 (s-p[i])**n + + returns r, p, k + """ + r,p,k = signal.residue(self.num, self.den, tol=tol) + if verbose>0: + print('r='+str(r)) + print('') + print('p='+str(p)) + print('') + print('k='+str(k)) + + return r, p, k + + + def PartFrac(self, eps=1.0e-12): + """Compute the partial fraction expansion based on the residue + command. In the final polynomials, coefficients whose + absolute values are less than eps are set to zero.""" + r,p,k = self.residue() + + rlist = r.tolist() + plist = p.tolist() + + N = len(rlist) + + tflist = [] + eps = 1e-12 + + while N > 0: + curr = rlist.pop(0) + curp = plist.pop(0) + if abs(curp.imag) < eps: + #This is a purely real pole. The portion of the partial + #fraction expansion corresponding to this pole is curr/(s-curp) + curtf = TransferFunction(curr,[1,-curp]) + else: + #this is a complex pole and we need to find its conjugate and + #handle them together + cind = plist.index(curp.conjugate()) + rconj = rlist.pop(cind) + pconj = plist.pop(cind) + p1 = poly1d([1,-curp]) + p2 = poly1d([1,-pconj]) + #num = curr*p2+rconj*p1 + Nr = curr.real + Ni = curr.imag + Pr = curp.real + Pi = curp.imag + numlist = [2.0*Nr,-2.0*(Nr*Pr+Ni*Pi)] + numlist = epslist(numlist, eps) + num = poly1d(numlist) + denlist = [1, -2.0*Pr,Pr**2+Pi**2] + denlist = epslist(denlist, eps) + den = poly1d(denlist) + curtf = TransferFunction(num,den) + tflist.append(curtf) + N = len(rlist) + return tflist + + + def FreqResp(self, f, fignum=1, fig=None, clear=True, \ + grid=True, legend=None, legloc=1, legsub=1, **kwargs): + """Compute the frequency response of the transfer function + using the frequency vector f, returning a complex vector. + + The frequency response (Bode plot) will be plotted on + figure(fignum) unless fignum=None. + + legend should be a list of legend entries if a legend is + desired. If legend is not None, the legend will be placed on + the top half of the plot (magnitude portion) if legsub=1, or + on the bottom half with legsub=2. legloc follows the same + rules as the pylab legend command (1 is top right and goes + counter-clockwise from there.)""" + testvect=real(f)==0 + if testvect.all(): + s=f#then you really sent me s and not f + else: + s=2.0j*pi*f + self.comp = self.num(s)/self.den(s) + self.dBmag = 20*log10(abs(self.comp)) + self.phase = angle(self.comp,1) + + if fig is None: + if fignum is not None: + import pylab + fig = pylab.figure(fignum) + + if fig is not None: + if clear: + fig.clf() + + if fig is not None: + myargs=['linetype','colors','linewidth'] + subkwargs={} + for key in myargs: + if kwargs.has_key(key): + subkwargs[key]=kwargs[key] + if clear: + fig.clf() + ax1 = fig.add_subplot(2,1,1) + #if clear: + # ax1.cla() + myind=ax1._get_lines.count + mylines=_PlotMag(f, self, axis=ax1, **subkwargs) + ax1.set_ylabel('Mag. Ratio (dB)') + ax1.xaxis.set_major_formatter(MyFormatter()) + if grid: + ax1.grid(1) + if legend is not None and legsub==1: + ax1.legend(legend, legloc) + ax2 = fig.add_subplot(2,1,2, sharex=ax1) + #if clear: + # ax2.cla() + mylines=_PlotPhase(f, self, axis=ax2, **subkwargs) + ax2.set_ylabel('Phase (deg.)') + ax2.set_xlabel('Freq. (Hz)') + ax2.xaxis.set_major_formatter(MyFormatter()) + if grid: + ax2.grid(1) + if legend is not None and legsub==2: + ax2.legend(legend, legloc) + return self.comp + + + def CrossoverFreq(self, f): + if not hasattr(self, 'dBmag'): + self.FreqResp(f, fignum=None) + t1 = squeeze(self.dBmag > 0.0) + t2 = r_[t1[1:],t1[0]] + t3 = (t1 & -t2) + myinds = where(t3)[0] + if not myinds.any(): + return None, [] + maxind = max(myinds) + return f[maxind], maxind + + + def PhaseMargin(self,f): + fc,ind=self.CrossoverFreq(f) + if not fc: + return 180.0 + return 180.0+squeeze(self.phase[ind]) + + + def create_tvect(self, dt=None, maxt=None): + if dt is None: + dt = self.dt + else: + self.dt = dt + assert dt is not None, "You must either pass in a dt or call create_tvect on an instance with a self.dt already defined." + if maxt is None: + if hasattr(self,'maxt'): + maxt = self.maxt + else: + maxt = 100*dt + else: + self.maxt = maxt + tvect = arange(0,maxt+dt/2.0,dt) + self.t = tvect + return tvect + + + def create_impulse(self, dt=None, maxt=None, imp_time=0.5): + """Create the input impulse vector to be used in least squares + curve fitting of the c2d function.""" + if dt is None: + dt = self.dt + indon = int(imp_time/dt) + tvect = self.create_tvect(dt=dt, maxt=maxt) + imp = zeros_like(tvect) + imp[indon] = 1.0 + return imp + + + def create_step_input(self, dt=None, maxt=None, indon=5): + """Create the input impulse vector to be used in least squares + curve fitting of the c2d function.""" + tvect = self.create_tvect(dt=dt, maxt=maxt) + mystep = zeros_like(tvect) + mystep[indon:] = 1.0 + return mystep + + + def step_response(self, t=None, dt=None, maxt=None, \ + step_time=None, fignum=1, clear=True, \ + plotu=False, amp=1.0, interp=0, fig=None, \ + fmts=['-','-'], legloc=0, returnall=0, \ + legend=None, **kwargs): + """Find the response of the system to a step input. If t is + not given, then the time vector will go from 0 to maxt in + steps of dt i.e. t=arange(0,maxt,dt). If dt and maxt are not + given, the parameters from the TransferFunction instance will + be used. + + step_time is the time when the step input turns on. If not + given, it will default to 0. + + If clear is True, the figure will be cleared first. + clear=False could be used to overlay the step responses of + multiple TransferFunction's. + + plotu=True means that the step input will also be shown on the + graph. + + amp is the amplitude of the step input. + + return y unless returnall is set then return y, t, u + + where y is the response of the transfer function, t is the + time vector, and u is the step input vector.""" + if t is not None: + tvect = t + else: + tvect = self.create_tvect(dt=dt, maxt=maxt) + u = zeros_like(tvect) + if dt is None: + dt = self.dt + if step_time is None: + step_time = 0.0 + #step_time = 0.1*tvect.max() + if kwargs.has_key('indon'): + indon = kwargs['indon'] + else: + indon = int(step_time/dt) + u[indon:] = amp + try: + ystep = self.lsim(u, tvect, interp=interp)#[1]#the outputs of lsim are (t, y,x) + except: + ystep = self.lsim2(u, tvect)#[1] + + if fig is None: + if fignum is not None: + import pylab + fig = pylab.figure(fignum) + + if fig is not None: + if clear: + fig.clf() + ax = fig.add_subplot(111) + if plotu: + leglist =['Input','Output'] + ax.plot(tvect, u, fmts[0], linestyle='steps', **kwargs)#assume step input wants 'steps' linestyle + ofmt = fmts[1] + else: + ofmt = fmts[0] + ax.plot(tvect, ystep, ofmt, **kwargs) + ax.set_ylabel('Step Response') + ax.set_xlabel('Time (sec)') + if legend is not None: + ax.legend(legend, loc=legloc) + elif plotu: + ax.legend(leglist, loc=legloc) + #return ystep, ax + #else: + #return ystep + if returnall: + return ystep, tvect, u + else: + return ystep + + + + def impulse_response(self, dt=None, maxt=None, fignum=1, \ + clear=True, amp=1.0, fig=None, \ + fmt='-', **kwargs): + """Find the impulse response of the system using + scipy.signal.impulse. + + The time vector will go from 0 to maxt in steps of dt + i.e. t=arange(0,maxt,dt). If dt and maxt are not given, the + parameters from the TransferFunction instance will be used. + + If clear is True, the figure will be cleared first. + clear=False could be used to overlay the impulse responses of + multiple TransferFunction's. + + amp is the amplitude of the impulse input. + + return y, t + + where y is the impulse response of the transfer function and t + is the time vector.""" + + tvect = self.create_tvect(dt=dt, maxt=maxt) + temptf = amp*self + tout, yout = temptf.impulse(T=tvect) + + if fig is None: + if fignum is not None: + import pylab + fig = pylab.figure(fignum) + + if fig is not None: + if clear: + fig.clf() + ax = fig.add_subplot(111) + ax.plot(tvect, yout, fmt, **kwargs) + ax.set_ylabel('Impulse Response') + ax.set_xlabel('Time (sec)') + + return yout, tout + + + def swept_sine_response(self, maxf, minf=0.0, dt=None, maxt=None, deadtime=2.0, interp=0): + u = create_swept_sine_input(maxt, dt, maxf, minf=minf, deadtime=deadtime) + t = create_swept_sine_t(maxt, dt, deadtime=deadtime) + ysweep = self.lsim(u, t, interp=interp) + return t, u, ysweep + + + def _c2d_sub(self, numsub, densub, scale): + """This method performs substitutions for continuous to + digital conversions using the form: + + numsub + s = scale* -------- + densub + + where scale is a floating point number and numsub and densub + are poly1d instances. + + For example, scale = 2.0/T, numsub = poly1d([1,-1]), and + densub = poly1d([1,1]) for a Tustin c2d transformation.""" + m = self.num.order + n = self.den.order + mynum = 0.0 + for p, coeff in enumerate(self.num.coeffs): + mynum += poly1d(coeff*(scale**(m-p))*((numsub**(m-p))*(densub**(n-(m-p))))) + myden = 0.0 + for p, coeff in enumerate(self.den.coeffs): + myden += poly1d(coeff*(scale**(n-p))*((numsub**(n-p))*(densub**(n-(n-p))))) + return mynum.coeffs, myden.coeffs + + + def c2d_tustin(self, dt=None, a=2.0): + """Convert a continuous time transfer function into a digital + one by substituting + + a z-1 + s = - ----- + T z+1 + + into the compensator, where a is typically 2.0""" + #print('in TransferFunction.c2d_tustin, dt=%s' % dt) + dt = self._get_set_dt(dt) + #print('in TransferFunction.c2d_tustin after _get_set_dt, dt=%s' % dt) + scale = a/dt + numsub = poly1d([1.0,-1.0]) + densub = poly1d([1.0,1.0]) + mynum, myden = self._c2d_sub(numsub, densub, scale) + mynum = mynum/myden[0] + myden = myden/myden[0] + return mynum, myden + + + + def c2d(self, dt=None, maxt=None, method='zoh', step_time=0.5, a=2.0): + """Find a numeric approximation of the discrete transfer + function of the system. + + The general approach is to find the response of the system + using lsim and fit a discrete transfer function to that + response as a least squares problem. + + dt is the time between discrete time intervals (i.e. the + sample time). + + maxt is the length of time for which to calculate the system + respnose. An attempt is made to guess an appropriate stopping + time if maxt is None. For now, this defaults to 100*dt, + assuming that dt is appropriate for the system poles. + + method is a string describing the c2d conversion algorithm. + method = 'zoh refers to a zero-order hold for a sampled-data + system and follows the approach outlined by Dorsey in section + 14.17 of + "Continuous and Discrete Control Systems" summarized on page + 472 of the 2002 edition. + + Other supported options for method include 'tustin' + + indon gives the index of when the step input should switch on + for zoh or when the impulse should happen otherwise. There + should probably be enough zero entries before the input occurs + to accomidate the order of the discrete transfer function. + + a is used only if method = 'tustin' and it is substituted in the form + + a z-1 + s = - ----- + T z+1 + + a is almost always equal to 2. + """ + if method.lower() == 'zoh': + ystep = self.step_response(dt=dt, maxt=maxt, step_time=step_time)[0] + myimp = self.create_impulse(dt=dt, maxt=maxt, imp_time=step_time) + #Pdb().set_trace() + print('You called c2d with "zoh". This is most likely bad.') + nz, dz = fit_discrete_response(ystep, myimp, self.den.order, self.den.order+1)#we want the numerator order to be one less than the denominator order - the denominator order +1 is the order of the denominator during a step response + #multiply by (1-z^-1) + nz2 = r_[nz, [0.0]] + nzs = r_[[0.0],nz] + nz3 = nz2 - nzs + nzout, dzout = polyfactor(nz3, dz) + return nzout, dzout + #return nz3, dz + elif method.lower() == 'tustin': + #The basic approach for tustin is to create a transfer + #function that represents s mapped into z and then + #substitute this s(z)=a/T*(z-1)/(z+1) into the continuous + #transfer function + return self.c2d_tustin(dt=dt, a=a) + else: + raise ValueError, 'c2d method not understood:'+str(method) + + + + def DigitalSim(self, u, method='zoh', bits=9, vmin=-2.5, vmax=2.5, dt=None, maxt=None, digitize=True): + """Simulate the digital reponse of the transfer to input u. u + is assumed to be an input signal that has been sampled with + frequency 1/dt. u is further assumed to be a floating point + number with precision much higher than bits. u will be + digitized over the range [min, max], which is broken up into + 2**bits number of bins. + + The A and B vectors from c2d conversion will be found using + method, dt, and maxt. Note that maxt is only used for + method='zoh'. + + Once A and B have been found, the digital reponse of the + system to the digitized input u will be found.""" + B, A = self.c2d(dt=dt, maxt=maxt, method=method) + assert A[0]==1.0, "A[0]!=1 in c2d result, A="+str(A) + uvect = zeros(len(B), dtype='d') + yvect = zeros(len(A)-1, dtype='d') + if digitize: + udig = ADC(u, bits, vmax=vmax, vmin=vmin) + dv = (vmax-vmin)/(2**bits-1) + else: + udig = u + dv = 1.0 + Ydig = zeros(len(u), dtype='d') + for n, u0 in enumerate(udig): + uvect = shift(uvect, u0) + curY = dot(uvect,B) + negpart = dot(yvect,A[1:]) + curY -= negpart + if digitize: + curY = int(curY) + Ydig[n] = curY + yvect = shift(yvect, curY) + return Ydig*dv + + + +class Input(TransferFunction): + def __repr__(self): + return TransferFunction.__repr__(self, labelstr='controls.Input') + + +class Compensator(TransferFunction): + def __init__(self, num, den, *args, **kwargs): + #print('in Compensator.__init__') + #Pdb().set_trace() + TransferFunction.__init__(self, num, den, *args, **kwargs) + + + def c2d(self, dt=None, a=2.0): + """Compensators should use Tustin for c2d conversion. This + method is just and alias for TransferFunction.c2d_tustin""" + #print('in Compensators.c2d, dt=%s' % dt) + #Pdb().set_trace() + return TransferFunction.c2d_tustin(self, dt=dt, a=a) + + def __repr__(self): + return TransferFunction.__repr__(self, labelstr='controls.Compensator') + + + +class Digital_Compensator(object): + def __init__(self, num, den, input_vect=None, output_vect=None): + self.num = num + self.den = den + self.input = input_vect + self.output = output_vect + self.Nnum = len(self.num) + self.Nden = len(self.den) + + + def calc_out(self, i): + out = 0.0 + for n, bn in enumerate(self.num): + out += self.input[i-n]*bn + + for n in range(1, self.Nden): + out -= self.output[i-n]*self.den[n] + out = out/self.den[0] + return out + + +class Digital_PI(object): + def __init__(self, kp, ki, input_vect=None, output_vect=None): + self.kp = kp + self.ki = ki + self.input = input_vect + self.output = output_vect + self.esum = 0.0 + + + def prep(self): + self.esum = zeros_like(self.input) + + + def calc_out(self, i): + self.esum[i] = self.esum[i-1]+self.input[i] + out = self.input[i]*self.kp+self.esum[i]*self.ki + return out + + +class Digital_P_Control(Digital_Compensator): + def __init__(self, kp, input_vect=None, output_vect=None): + self.kp = kp + self.input = input_vect + self.output = output_vect + self.num = poly1d([kp]) + self.den = poly1d([1]) + self.gain = 1 + + def calc_out(self, i): + self.output[i] = self.kp*self.input[i] + return self.output[i] + + +def dig_comp_from_c_comp(c_comp, dt): + """Convert a continuous compensator into a digital one using Tustin + and sampling time dt.""" + b, a = c_comp.c2d_tustin(dt=dt) + return Digital_Compensator(b, a) + + +class FirstOrderCompensator(Compensator): + def __init__(self, K, z, p, dt=0.004): + """Create a first order compensator whose transfer function is + + K*(s+z) + D(s) = ----------- + (s+p) """ + Compensator.__init__(self, K*poly1d([1,z]), [1,p]) + + + def __repr__(self): + return TransferFunction.__repr__(self, labelstr='controls.FirstOrderCompensator') + + + def ToPSoC(self, dt=0.004): + b, a = self.c2d(dt=dt) + outstr = 'v = %f*e%+f*ep%+f*vp;'%(b[0],b[1],-a[1]) + print('PSoC str:') + print(outstr) + return outstr + + +def sat(vin, vmax=2.0): + if vin > vmax: + return vmax + elif vin < -1*vmax: + return -1*vmax + else: + return vin + + + +class Closed_Loop_System_with_Sat(object): + def __init__(self, plant_tf, Kp, sat): + self.plant_tf = plant_tf + self.Kp = Kp + self.sat = sat + + + def lsim(self, u, t, X0=None, include_sat=True, \ + returnall=0, lsim2=0, verbosity=0): + dt = t[1]-t[0] + if X0 is None: + X0 = zeros((2,len(self.plant_tf.den.coeffs)-1)) + N = len(t) + y = zeros(N) + v = zeros(N) + x_n = X0 + for n in range(1,N): + t_n = t[n] + if verbosity > 0: + print('t_n='+str(t_n)) + e = u[n]-y[n-1] + v_n = self.Kp*e + if include_sat: + v_n = sat(v_n, vmax=self.sat) + #simulate for one dt using ZOH + if lsim2: + t_nn, y_n, x_n = self.plant_tf.lsim2([v_n,v_n], [t_n, t_n+dt], X0=x_n[-1], returnall=1) + else: + t_nn, y_n, x_n = self.plant_tf.lsim([v_n,v_n], [t_n, t_n+dt], X0=x_n[-1], returnall=1) + + y[n] = y_n[-1] + v[n] = v_n + self.y = y + self.v = v + self.u = u + if returnall: + return y, v + else: + return y + + + + + +def step_input(): + return Input(1,[1,0]) + + +def feedback(olsys,H=1): + """Calculate the closed-loop transfer function + + olsys + cltf = -------------- + 1+H*olsys + + where olsys is the transfer function of the open loop + system (Gc*Gp) and H is the transfer function in the feedback + loop (H=1 for unity feedback).""" + clsys=olsys/(1.0+H*olsys) + return clsys + + + +def Usweep(ti,maxt,minf=0.0,maxf=10.0): + """Return the current value (scalar) of a swept sine signal - must be used + with list comprehension to generate a vector. + + ti - current time (scalar) + minf - lowest frequency in the sweep + maxf - highest frequency in the sweep + maxt - T or the highest value in the time vector""" + if ti<0.0: + return 0.0 + else: + curf=(maxf-minf)*ti/maxt+minf + if ti<(maxt*0.95): + return sin(2*pi*curf*ti) + else: + return 0.0 + + +def sweptsine(t,minf=0.0, maxf=10.0): + """Generate a sweptsine vector by calling Usweep for each ti in t.""" + T=max(t)-min(t) + Us = [Usweep(ti,T,minf,maxf) for ti in t] + return array(Us) + + +mytypes=['-','--',':','-.'] +colors=['b','y','r','g','c','k']#['y','b','r','g','c','k'] + +def _getlinetype(ax=None): + if ax is None: + import pylab + ax = pylab.gca() + myind=ax._get_lines.count + return {'color':colors[myind % len(colors)],'linestyle':mytypes[myind % len(mytypes)]} + + +def create_step_vector(t, step_time=0.0, amp=1.0): + u = zeros_like(t) + dt = t[1]-t[0] + indon = int(step_time/dt) + u[indon:] = amp + return u + + +def rate_limiter(uin, du): + uout = zeros_like(uin) + N = len(uin) + for n in range(1,N): + curchange = uin[n]-uout[n-1] + if curchange > du: + uout[n] = uout[n-1]+du + elif curchange < -du: + uout[n] = uout[n-1]-du + else: + uout[n] = uin[n] + return uout + + + + This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2010-06-18 05:06:36
|
Revision: 27 http://python-control.svn.sourceforge.net/python-control/?rev=27&view=rev Author: murrayrm Date: 2010-06-18 05:06:29 +0000 (Fri, 18 Jun 2010) Log Message: ----------- Extracted Ryan's root locus code to see how things work. Main change was converting num, den to poly1d's (assumed in Ryan's TF class). Basic functionality seems to work (!). Modified Paths: -------------- trunk/external/controls.py Added Paths: ----------- trunk/src/rlocus.py Modified: trunk/external/controls.py =================================================================== --- trunk/external/controls.py 2010-06-17 21:29:23 UTC (rev 26) +++ trunk/external/controls.py 2010-06-18 05:06:29 UTC (rev 27) @@ -1,5 +1,5 @@ # controls.py - Ryan Krauss's control module -# $Id$ +# $Id: $ """This module is for analyzing linear, time-invariant dynamic systems and feedback control systems using the Laplace transform. The heart Added: trunk/src/rlocus.py =================================================================== --- trunk/src/rlocus.py (rev 0) +++ trunk/src/rlocus.py 2010-06-18 05:06:29 UTC (rev 27) @@ -0,0 +1,140 @@ +# rlocus.py - code for computing a root locus plot +# Code contributed by Ryan Krauss, 2010 +# +# Copyright (c) 2010 by Ryan Krauss +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the California Institute of Technology nor +# the names of its contributors may be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# RMM, 17 June 2010: modified to be a standalone piece of code +# * Added BSD copyright info to file (per Ryan) +# * Added code to convert (num, den) to poly1d's if they aren't already. +# This allows Ryan's code to run on a standard signal.ltisys object +# or a controls.TransferFunction object. +# * Added some comments to make sure I understand the code +# +# $Id: $ + +# Packages used by this module +from scipy import * + +# Main function: compute a root locus diagram +def RootLocus(sys, kvect, fig=None, fignum=1, \ + clear=True, xlim=None, ylim=None, plotstr='-'): + """Calculate the root locus by finding the roots of 1+k*TF(s) + where TF is self.num(s)/self.den(s) and each k is an element + of kvect.""" + + # Convert numerator and denominator to polynomials if they aren't + (num, den) = _systopoly1d(sys); + + # Set up the figure + if fig is None: + import pylab + fig = pylab.figure(fignum) + if clear: + fig.clf() + ax = fig.add_subplot(111) + + # Compute out the loci + mymat = _RLFindRoots(sys, kvect) + mymat = _RLSortRoots(sys, mymat) + + # plot open loop poles + poles = array(den.r) + ax.plot(real(poles), imag(poles), 'x') + + # plot open loop zeros + zeros = array(num.r) + if zeros.any(): + ax.plot(real(zeros), imag(zeros), 'o') + + # Now plot the loci + for col in mymat.T: + ax.plot(real(col), imag(col), plotstr) + + # Set up plot axes and labels + if xlim: + ax.set_xlim(xlim) + if ylim: + ax.set_ylim(ylim) + ax.set_xlabel('Real') + ax.set_ylabel('Imaginary') + return mymat + +# Utility function to extract numerator and denominator polynomials +def _systopoly1d(sys): + """Extract numerator and denominator polynomails for a system""" + + # Start by extracting the numerator and denominator from system object + num = sys.num; den = sys.den; + + # Check to see if num, den are already polynomials; otherwise convert + if (not isinstance(num, poly1d)): num = poly1d(num) + if (not isinstance(den, poly1d)): den = poly1d(den) + + return (num, den) + +def _RLFindRoots(sys, kvect): + """Find the roots for the root locus.""" + + # Convert numerator and denominator to polynomials if they aren't + (num, den) = _systopoly1d(sys); + + roots = [] + for k in kvect: + curpoly = den + k * num + curroots = curpoly.r + curroots.sort() + roots.append(curroots) + mymat = row_stack(roots) + return mymat + +def _RLSortRoots(sys, mymat): + """Sort the roots from sys._RLFindRoots, so that the root + locus doesn't show weird pseudo-branches as roots jump from + one branch to another.""" + + sorted = zeros_like(mymat) + for n, row in enumerate(mymat): + if n==0: + sorted[n,:] = row + else: + # sort the current row by finding the element with the + # smallest absolute distance to each root in the + # previous row + available = range(len(prevrow)) + for elem in row: + evect = elem-prevrow[available] + ind1 = abs(evect).argmin() + ind = available.pop(ind1) + sorted[n,ind] = elem + prevrow = sorted[n,:] + return sorted This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2010-06-18 05:10:32
|
Revision: 28 http://python-control.svn.sourceforge.net/python-control/?rev=28&view=rev Author: murrayrm Date: 2010-06-18 05:10:26 +0000 (Fri, 18 Jun 2010) Log Message: ----------- set svn:keywords to Id to get tag working Modified Paths: -------------- trunk/external/controls.py trunk/src/rlocus.py Property Changed: ---------------- trunk/external/controls.py trunk/src/rlocus.py Modified: trunk/external/controls.py =================================================================== --- trunk/external/controls.py 2010-06-18 05:06:29 UTC (rev 27) +++ trunk/external/controls.py 2010-06-18 05:10:26 UTC (rev 28) @@ -1,5 +1,5 @@ # controls.py - Ryan Krauss's control module -# $Id: $ +# $Id$ """This module is for analyzing linear, time-invariant dynamic systems and feedback control systems using the Laplace transform. The heart Property changes on: trunk/external/controls.py ___________________________________________________________________ Added: svn:keywords + Id Modified: trunk/src/rlocus.py =================================================================== --- trunk/src/rlocus.py 2010-06-18 05:06:29 UTC (rev 27) +++ trunk/src/rlocus.py 2010-06-18 05:10:26 UTC (rev 28) @@ -40,7 +40,7 @@ # or a controls.TransferFunction object. # * Added some comments to make sure I understand the code # -# $Id: $ +# $Id$ # Packages used by this module from scipy import * Property changes on: trunk/src/rlocus.py ___________________________________________________________________ Added: svn:keywords + Id This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2011-04-03 04:55:59
|
Revision: 152 http://python-control.svn.sourceforge.net/python-control/?rev=152&view=rev Author: murrayrm Date: 2011-04-03 04:55:51 +0000 (Sun, 03 Apr 2011) Log Message: ----------- Small fixes based on testing + re-added root locus * Implemented syntax checks for MATLAB functions * Fixed root locus plots to work with new SF, TF classes * Transfer functions with integer coeffs now convered to floats * More detailed list of changes in ChangeLog Modified Paths: -------------- trunk/ChangeLog trunk/Pending trunk/examples/secord-matlab.py trunk/src/__init__.py trunk/src/exception.py trunk/src/freqplot.py trunk/src/matlab.py trunk/src/rlocus.py trunk/src/xferfcn.py trunk/tests/matlab_test.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-04-02 19:32:33 UTC (rev 151) +++ trunk/ChangeLog 2011-04-03 04:55:51 UTC (rev 152) @@ -1,5 +1,38 @@ 2011-04-02 Richard Murray <murray@malabar.local> + * src/xferfcn.py (TransferFunction.__add__): fixed bug when adding a + transfer function to state space system; _convertToTransferFunction + was being called with input/output keywords. Picked up in unit test. + + * tests/matlab_test.py: added calls to all of the functions that are + currently implemented in the library, to make sure there are no + hidden issues. These calls do *not* test functionality, they just + make sure that MATLAB compatibility functions accept the right types + of arguments. + + * examples/secord-matlab.py: added root locus plot to list of + figures that are produced + + * src/__init__.py: added rlocus to list of modules that are imported + by control module + + * src/exception.py (ControlMIMONotImplemented): added exception for + functions that are not yet implemented for MIMO systems + + * src/xferfcn.py (TransferFunction.__init__): convert integer + numerator and denominator objects to floats to eliminate errors when + creating common denominator (also used on pole command). This fixes + and error were tf([1], [1, 2, 1]).pole() would generate an error. + + * src/freqplot.py (bode): Tweaked documentation string to remove 'h' + from mag and phase return arguments + + * src/rlocus.py (RootLocus): removed commands that set figure number + inside of RootLocus command, for consistency with other frequency + plot routines. Added Plot=True argument and updated documentation. + + * src/matlab.py: added rlocus() command (calls RootLocus) + * MANIFEST.in: Added MANIFEST.in file to include examples and tests in source distribution Modified: trunk/Pending =================================================================== --- trunk/Pending 2011-04-02 19:32:33 UTC (rev 151) +++ trunk/Pending 2011-04-03 04:55:51 UTC (rev 152) @@ -12,7 +12,7 @@ * matlab.step() doesn't handle systems with a pole at the origin (use lsim2) * TF <-> SS transformations are buggy; see tests/convert_test.py * hsvd returns different value than MATLAB (2010a); see modelsimp_test.py - * MIMO common denominator fails unit test; see convert_test.py + * lsim doesn't work for StateSpace systems (signal.lsim2 bug??) Transfer code from Roberto Bucher's yottalab to python-control acker - pole placement using Ackermann method @@ -44,23 +44,15 @@ * tests/freqresp.py needs to be converted to unit test * Convert examples/test-{response,statefbk}.py to unit tests -TransferFunction class fixes - * evalfr is not working (num, den stored as ndarrays, not poly1ds) +Root locus plot improvements + * Make sure that scipy.signal.lti objects still work + * Update calling syntax to be consistent with other plotting commands -Block diagram algebra fixes - * Implement state space block diagram algebra - * Produce minimal realizations to avoid later errors - State space class fixes - * Convert pvtol to state space systems and rerun example * Implement pzmap for state space systems -LTI updates - * Implement control.matlab.step (with semantics similar to MATLAB) - Basic functions to be added * margin - compute gain and phase margin (no plot) - * lqr - compute optimal feedback gains (use SLICOT SB02ND.f) * lyap - solve Lyapunov equation (use SLICOT SB03MD.f) * See http://www.slicot.org/shared/libindex.html for list of functions Modified: trunk/examples/secord-matlab.py =================================================================== --- trunk/examples/secord-matlab.py 2011-04-02 19:32:33 UTC (rev 151) +++ trunk/examples/secord-matlab.py 2011-04-03 04:55:51 UTC (rev 152) @@ -27,3 +27,7 @@ # Nyquist plot for the system figure(3) nyquist(sys, logspace(-2, 2)) + +# Root lcous plut for the system +figure(4) +rlocus(sys) Modified: trunk/src/__init__.py =================================================================== --- trunk/src/__init__.py 2011-04-02 19:32:33 UTC (rev 151) +++ trunk/src/__init__.py 2011-04-03 04:55:51 UTC (rev 152) @@ -64,3 +64,4 @@ from statefbk import * from delay import * from modelsimp import * +from rlocus import * Modified: trunk/src/exception.py =================================================================== --- trunk/src/exception.py 2011-04-02 19:32:33 UTC (rev 151) +++ trunk/src/exception.py 2011-04-03 04:55:51 UTC (rev 152) @@ -52,6 +52,10 @@ """Raised when arguments to a function are not correct""" pass +class ControlMIMONotImplemented(Exception): + """Function is not currently implemented for MIMO systems""" + pass + class ControlNotImplemented(Exception): """Functionality is not yet implemented""" pass Modified: trunk/src/freqplot.py =================================================================== --- trunk/src/freqplot.py 2011-04-02 19:32:33 UTC (rev 151) +++ trunk/src/freqplot.py 2011-04-03 04:55:51 UTC (rev 152) @@ -60,7 +60,7 @@ Usage ===== - (magh, phaseh, omega) = bode(syslist, omega=None, dB=False, Hz=False, color=None, Plot=True) + (mag, phase, omega) = bode(syslist, omega=None, dB=False, Hz=False, color=None, Plot=True) Plots a Bode plot for the system over a (optional) frequency range. @@ -79,8 +79,8 @@ Return values ------------- - magh : magnitude array - phaseh : phase array + mag : magnitude array + phase : phase array omega : frequency array Notes Modified: trunk/src/matlab.py =================================================================== --- trunk/src/matlab.py 2011-04-02 19:32:33 UTC (rev 151) +++ trunk/src/matlab.py 2011-04-03 04:55:51 UTC (rev 152) @@ -179,7 +179,7 @@ \* ss/modred - model order reduction Compensator design - rlocus - evans root locus +\* rlocus - evans root locus \* place - pole placement estim - form estimator given estimator gain reg - form regulator given state-feedback and estimator gains @@ -187,7 +187,7 @@ LQR/LQG design ss/lqg - single-step LQG design \* lqr - linear-Quadratic (LQ) state-feedback regulator -\* dlqr - discrete-time LQ state-feedback regulator + dlqr - discrete-time LQ state-feedback regulator lqry - lq regulator with output weighting lqrd - discrete LQ regulator for continuous plant ss/lqi - linear-Quadratic-Integral (LQI) controller @@ -243,8 +243,8 @@ Overloaded arithmetic operations \* + and - - add and subtract systems (parallel connection) \* \* - multiply systems (series connection) -\* / - left divide -- sys1\sys2 means inv(sys1)\*sys2 -\* / - right divide -- sys1/sys2 means sys1\*inv(sys2) + / - left divide -- sys1\sys2 means inv(sys1)\*sys2 +- \ - right divide -- sys1/sys2 means sys1\*inv(sys2) ^ - powers of a given system ' - pertransposition .' - transposition of input/output map @@ -758,12 +758,35 @@ from nichols import nichols_grid nichols_grid() +# Root locus plot +def rlocus(sys, klist = None, **keywords): + """Root locus plot + + Parameters + ---------- + sys: StateSpace or TransferFunction object + klist: optional list of gains + + Returns + ------- + rlist: list of roots for each gain + klist: list of gains used to compute roots + """ + from rlocus import RootLocus + if (klist == None): + #! TODO: update with a smart cacluation of the gains + klist = logspace(-3, 3) + + rlist = RootLocus(sys, klist, **keywords) + return rlist, klist + + # # Modifications to scipy.signal functions # # Redefine lsim to use lsim2 -def lsim(*args, **keywords): +def lsim(sys, U=None, T=None, X0=None, **keywords): """Simulate the output of a linear system Examples @@ -784,12 +807,13 @@ yout: response of the system xout: time evolution of the state vector """ - sys = args[0] + # Convert the system to an signal.lti for simulation + #! This should send a warning for MIMO systems ltiobjs = sys.returnScipySignalLti() ltiobj = ltiobjs[0][0] - - return sp.signal.lsim2(ltiobj, **keywords) + return sp.signal.lsim2(ltiobj, U, T, X0, **keywords) + #! Redefine step to use lsim2 #! Not yet implemented def step(*args, **keywords): Modified: trunk/src/rlocus.py =================================================================== --- trunk/src/rlocus.py 2011-04-02 19:32:33 UTC (rev 151) +++ trunk/src/rlocus.py 2011-04-03 04:55:51 UTC (rev 152) @@ -37,65 +37,93 @@ # * Added BSD copyright info to file (per Ryan) # * Added code to convert (num, den) to poly1d's if they aren't already. # This allows Ryan's code to run on a standard signal.ltisys object -# or a controls.TransferFunction object. +# or a control.TransferFunction object. # * Added some comments to make sure I understand the code +# +# RMM, 2 April 2011: modified to work with new Lti structure (see ChangeLog) +# * Not tested: should still work on signal.ltisys objects # # $Id$ # Packages used by this module from scipy import * +import scipy.signal # signal processing toolbox +import pylab # plotting routines +import xferfcn # transfer function manipulation # Main function: compute a root locus diagram -def RootLocus(sys, kvect, fig=None, fignum=1, \ - clear=True, xlim=None, ylim=None, plotstr='-'): +def RootLocus(sys, kvect, xlim=None, ylim=None, plotstr='-', Plot=True): """Calculate the root locus by finding the roots of 1+k*TF(s) where TF is self.num(s)/self.den(s) and each k is an element - of kvect.""" + of kvect. + Parameters + ---------- + sys : linsys + Linear input/output systems (SISO only, for now) + klist : gain_range (default = None) + List of gains to use in computing diagram + Plot : boolean (default = True) + If True, plot magnitude and phase + + Return values + ------------- + rlist : list of computed root locations + klist : list of gains + """ + # Convert numerator and denominator to polynomials if they aren't (nump, denp) = _systopoly1d(sys); - # Set up the figure - if fig is None: - import pylab - fig = pylab.figure(fignum) - if clear: - fig.clf() - ax = fig.add_subplot(111) - # Compute out the loci mymat = _RLFindRoots(sys, kvect) mymat = _RLSortRoots(sys, mymat) - # plot open loop poles - poles = array(denp.r) - ax.plot(real(poles), imag(poles), 'x') + # Create the plot + if (Plot): + ax = pylab.axes(); - # plot open loop zeros - zeros = array(nump.r) - if zeros.any(): - ax.plot(real(zeros), imag(zeros), 'o') + # plot open loop poles + poles = array(denp.r) + ax.plot(real(poles), imag(poles), 'x') - # Now plot the loci - for col in mymat.T: - ax.plot(real(col), imag(col), plotstr) + # plot open loop zeros + zeros = array(nump.r) + if zeros.any(): + ax.plot(real(zeros), imag(zeros), 'o') - # Set up plot axes and labels - if xlim: - ax.set_xlim(xlim) - if ylim: - ax.set_ylim(ylim) - ax.set_xlabel('Real') - ax.set_ylabel('Imaginary') + # Now plot the loci + for col in mymat.T: + ax.plot(real(col), imag(col), plotstr) + + # Set up plot axes and labels + if xlim: + ax.set_xlim(xlim) + if ylim: + ax.set_ylim(ylim) + ax.set_xlabel('Real') + ax.set_ylabel('Imaginary') + return mymat # Utility function to extract numerator and denominator polynomials def _systopoly1d(sys): """Extract numerator and denominator polynomails for a system""" + # Allow inputs from the signal processing toolbox + if (isinstance(sys, scipy.signal.lti)): + nump = sys.num; denp = sys.den; - # Start by extracting the numerator and denominator from system object - nump = sys.num; denp = sys.den; + else: + # Convert to a transfer function, if needed + sys = xferfcn._convertToTransferFunction(sys) + # Make sure we have a SISO system + if (sys.inputs > 1 or sys.outputs > 1): + raise ControlMIMONotImplemented() + + # Start by extracting the numerator and denominator from system object + nump = sys.num[0][0]; denp = sys.den[0][0]; + # Check to see if num, den are already polynomials; otherwise convert if (not isinstance(nump, poly1d)): nump = poly1d(nump) if (not isinstance(denp, poly1d)): denp = poly1d(denp) Modified: trunk/src/xferfcn.py =================================================================== --- trunk/src/xferfcn.py 2011-04-02 19:32:33 UTC (rev 151) +++ trunk/src/xferfcn.py 2011-04-03 04:55:51 UTC (rev 152) @@ -129,11 +129,20 @@ for i in range(len(data)): if isinstance(data[i], (int, float, long, complex)): # Convert scalar to list of list of array. - data[i] = [[array([data[i]])]] + if (isinstance(data[i], int)): + # Convert integers to floats at this point + data[i] = [[array([data[i]], dtype=float)]] + else: + data[i] = [[array([data[i]])]] elif (isinstance(data[i], (list, tuple, ndarray)) and isinstance(data[i][0], (int, float, long, complex))): # Convert array to list of list of array. - data[i] = [[array(data[i])]] + if (isinstance(data[i][0], int)): + # Convert integers to floats at this point + #! Not sure this covers all cases correctly + data[i] = [[array(data[i], dtype=float)]] + else: + data[i] = [[array(data[i])]] elif (isinstance(data[i], list) and isinstance(data[i][0], list) and isinstance(data[i][0][0], (list, tuple, ndarray)) and @@ -142,7 +151,10 @@ # coefficient vectors to arrays, if necessary. for j in range(len(data[i])): for k in range(len(data[i][j])): - data[i][j][k] = array(data[i][j][k]) + if (isinstance(data[i][j][k], int)): + data[i][j][k] = array(data[i][j][k], dtype=float) + else: + data[i][j][k] = array(data[i][j][k]) else: # If the user passed in anything else, then it's unclear what # the meaning is. @@ -274,7 +286,9 @@ """Add two LTI objects (parallel connection).""" # Convert the second argument to a transfer function. - if not isinstance(other, TransferFunction): + if (isinstance(other, statesp.StateSpace)): + other = _convertToTransferFunction(other) + elif not isinstance(other, TransferFunction): other = _convertToTransferFunction(other, inputs=self.inputs, outputs=self.outputs) @@ -513,6 +527,7 @@ # A sorted list to keep track of cumulative poles found as we scan # self.den. poles = [] + # A 3-D list to keep track of common denominator poles not present in # the self.den[i][j]. missingpoles = [[[] for j in range(self.inputs)] for i in Modified: trunk/tests/matlab_test.py =================================================================== --- trunk/tests/matlab_test.py 2011-04-02 19:32:33 UTC (rev 151) +++ trunk/tests/matlab_test.py 2011-04-03 04:55:51 UTC (rev 152) @@ -2,47 +2,229 @@ # # matlab_test.py - test MATLAB compatibility # RMM, 30 Mar 2011 (based on TestMatlab from v0.4a) +# +# This test suite just goes through and calls all of the MATLAB +# functions using different systems and arguments to make sure that +# nothing crashes. It doesn't test actual functionality; the module +# specific unit tests will do that. import unittest import numpy as np from control.matlab import * class TestMatlab(unittest.TestCase): - def testStep(self): + def setUp(self): + """Set up some systems for testing out MATLAB functions""" A = np.matrix("1. -2.; 3. -4.") B = np.matrix("5.; 7.") C = np.matrix("6. 8.") D = np.matrix("9.") - sys = ss(A,B,C,D) + self.siso_ss1 = ss(A,B,C,D) + + # Create some transfer functions + self.siso_tf1 = tf([1], [1, 2, 1]); + self.siso_tf2 = tf([1, 1], [1, 2, 3, 1]); + + # Conversions + self.siso_tf3 = tf(self.siso_ss1); + self.siso_ss2 = ss(self.siso_tf2); + self.siso_ss3 = tf2ss(self.siso_tf3); + self.siso_tf4 = ss2tf(self.siso_ss2); + + def testParallel(self): + sys1 = parallel(self.siso_ss1, self.siso_ss2) + sys1 = parallel(self.siso_ss1, self.siso_tf2) + sys1 = parallel(self.siso_tf1, self.siso_ss2) + sys1 = parallel(1, self.siso_ss2) + sys1 = parallel(1, self.siso_tf2) + sys1 = parallel(self.siso_ss1, 1) + sys1 = parallel(self.siso_tf1, 1) + + def testSeries(self): + sys1 = series(self.siso_ss1, self.siso_ss2) + sys1 = series(self.siso_ss1, self.siso_tf2) + sys1 = series(self.siso_tf1, self.siso_ss2) + sys1 = series(1, self.siso_ss2) + sys1 = series(1, self.siso_tf2) + sys1 = series(self.siso_ss1, 1) + sys1 = series(self.siso_tf1, 1) + + def testFeedback(self): + sys1 = feedback(self.siso_ss1, self.siso_ss2) + sys1 = feedback(self.siso_ss1, self.siso_tf2) + sys1 = feedback(self.siso_tf1, self.siso_ss2) + sys1 = feedback(1, self.siso_ss2) + sys1 = feedback(1, self.siso_tf2) + sys1 = feedback(self.siso_ss1, 1) + sys1 = feedback(self.siso_tf1, 1) + + def testPoleZero(self): + pole(self.siso_ss1); + pole(self.siso_tf1); + pole(self.siso_tf2); + zero(self.siso_ss1); + zero(self.siso_tf1); + zero(self.siso_tf2); + + def testPZmap(self): + # pzmap(self.siso_ss1); not implemented + # pzmap(self.siso_ss2); not implemented + pzmap(self.siso_tf1); + pzmap(self.siso_tf2); + pzmap(self.siso_tf2, Plot=False); + + def testStep(self): + sys = self.siso_ss1 t = np.linspace(0, 1, 10) t, yout = step(sys, T=t) youttrue = np.matrix("9. 17.6457 24.7072 30.4855 35.2234 39.1165 42.3227 44.9694 47.1599 48.9776") np.testing.assert_array_almost_equal(yout, youttrue,decimal=4) def testImpulse(self): - A = np.matrix("1. -2.; 3. -4.") - B = np.matrix("5.; 7.") - C = np.matrix("6. 8.") - D = np.matrix("9.") - sys = ss(A,B,C,D) + sys = self.siso_ss1 t = np.linspace(0, 1, 10) t, yout = impulse(sys, T=t) youttrue = np.matrix("86. 70.1808 57.3753 46.9975 38.5766 31.7344 26.1668 21.6292 17.9245 14.8945") np.testing.assert_array_almost_equal(yout, youttrue,decimal=4) # def testInitial(self): -# A = np.matrix("1. -2.; 3. -4.") -# B = np.matrix("5.; 7.") -# C = np.matrix("6. 8.") -# D = np.matrix("9.") -# sys = ss(A,B,C,D) + sys = self.siso_ss1 # t = np.linspace(0, 1, 10) # x0 = np.matrix(".5; 1.") # t, yout = initial(sys, T=t, X0=x0) # youttrue = np.matrix("11. 8.1494 5.9361 4.2258 2.9118 1.9092 1.1508 0.5833 0.1645 -0.1391") # np.testing.assert_array_almost_equal(yout, youttrue,decimal=4) + def testLsim(self): + T = range(1, 100) + u = np.sin(T) + lsim(self.siso_tf1, u, T) + # lsim(self.siso_ss1, u, T) # generates error?? + # lsim(self.siso_ss1, u, T, self.siso_ss1.B) + def testBode(self): + bode(self.siso_ss1) + bode(self.siso_tf1) + bode(self.siso_tf2) + (mag, phase, freq) = bode(self.siso_tf2, Plot=False) + bode(self.siso_tf1, self.siso_tf2) + w = logspace(-3, 3); + bode(self.siso_ss1, w) + bode(self.siso_ss1, self.siso_tf2, w) + bode(self.siso_ss1, '-', self.siso_tf1, 'b--', self.siso_tf2, 'k.') + + def testRlocus(self): + rlocus(self.siso_ss1) + rlocus(self.siso_tf1) + rlocus(self.siso_tf2) + rlist, klist = rlocus(self.siso_tf2, klist=[1, 10, 100], Plot=False) + + def testNyquist(self): + nyquist(self.siso_ss1) + nyquist(self.siso_tf1) + nyquist(self.siso_tf2) + w = logspace(-3, 3); + nyquist(self.siso_tf2, w) + (real, imag, freq) = nyquist(self.siso_tf2, w, Plot=False) + + def testNichols(self): + nichols(self.siso_ss1) + nichols(self.siso_tf1) + nichols(self.siso_tf2) + w = logspace(-3, 3); + nichols(self.siso_tf2, w) + nichols(self.siso_tf2, grid=False) + + def testFreqresp(self): + w = logspace(-3, 3) + freqresp(self.siso_ss1, w) + freqresp(self.siso_ss2, w) + freqresp(self.siso_ss3, w) + freqresp(self.siso_tf1, w) + freqresp(self.siso_tf2, w) + freqresp(self.siso_tf3, w) + + def testEvalfr(self): + w = 1 + evalfr(self.siso_ss1, w) + evalfr(self.siso_ss2, w) + evalfr(self.siso_ss3, w) + evalfr(self.siso_tf1, w) + evalfr(self.siso_tf2, w) + evalfr(self.siso_tf3, w) + + def testHsvd(self): + hsvd(self.siso_ss1) + hsvd(self.siso_ss2) + hsvd(self.siso_ss3) + + def testBalred(self): + balred(self.siso_ss1, 1) + balred(self.siso_ss2, 2) + balred(self.siso_ss3, [2, 2]) + + def testModred(self): + modred(self.siso_ss1, [1]) + modred(self.siso_ss2 * self.siso_ss3, [1, 2]) + modred(self.siso_ss3, [1], 'matchdc') + modred(self.siso_ss3, [1], 'truncate') + + def testPlace(self): + place(self.siso_ss1.A, self.siso_ss1.B, [-2, -2]) + + def testLQR(self): + (K, S, E) = lqr(self.siso_ss1.A, self.siso_ss1.B, np.eye(2), np.eye(1)) + (K, S, E) = lqr(self.siso_ss2.A, self.siso_ss2.B, np.eye(3), \ + np.eye(1), [[1], [1], [2]]) + + def testRss(self): + rss(1) + rss(2) + rss(2, 3, 1) + + def testDrss(self): + drss(1) + drss(2) + drss(2, 3, 1) + + def testCtrb(self): + ctrb(self.siso_ss1.A, self.siso_ss1.B) + ctrb(self.siso_ss2.A, self.siso_ss2.B) + + def testObsv(self): + obsv(self.siso_ss1.A, self.siso_ss1.C) + obsv(self.siso_ss2.A, self.siso_ss2.C) + + def testGram(self): + gram(self.siso_ss1, 'c') + gram(self.siso_ss2, 'c') + gram(self.siso_ss1, 'o') + gram(self.siso_ss2, 'o') + + def testPade(self): + pade(1, 1) + pade(1, 2) + pade(5, 4) + + def testOpers(self): + self.siso_ss1 + self.siso_ss2 + self.siso_tf1 + self.siso_tf2 + self.siso_ss1 + self.siso_tf2 + self.siso_tf1 + self.siso_ss2 + self.siso_ss1 * self.siso_ss2 + self.siso_tf1 * self.siso_tf2 + self.siso_ss1 * self.siso_tf2 + self.siso_tf1 * self.siso_ss2 + # self.siso_ss1 / self.siso_ss2 not implemented yet + # self.siso_tf1 / self.siso_tf2 + # self.siso_ss1 / self.siso_tf2 + # self.siso_tf1 / self.siso_ss2 + + def testUnwrap(self): + phase = np.array(range(1, 100)) / 10.; + wrapped = phase % (2 * np.pi) + unwrapped = unwrap(wrapped) + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestMatlab) This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <gor...@us...> - 2011-04-16 23:45:52
|
Revision: 155 http://python-control.svn.sourceforge.net/python-control/?rev=155&view=rev Author: goretkin Date: 2011-04-16 23:45:46 +0000 (Sat, 16 Apr 2011) Log Message: ----------- Support for the **,/ operator. >> s = control.tf.TransferFunction([1, 0],[1]) sys = s/(s**2+s+1) #works as expected Modified Paths: -------------- trunk/setup.py trunk/src/xferfcn.py Modified: trunk/setup.py =================================================================== --- trunk/setup.py 2011-04-07 20:12:28 UTC (rev 154) +++ trunk/setup.py 2011-04-16 23:45:46 UTC (rev 155) @@ -1,6 +1,7 @@ #!/usr/bin/env python -from distutils.core import setup +#from distutils.core import setup +from setuptools import setup setup(name = 'control', version = '0.4c', Modified: trunk/src/xferfcn.py =================================================================== --- trunk/src/xferfcn.py 2011-04-07 20:12:28 UTC (rev 154) +++ trunk/src/xferfcn.py 2011-04-16 23:45:46 UTC (rev 155) @@ -372,9 +372,13 @@ def __div__(self, other): """Divide two LTI objects.""" - # Convert the second argument to a transfer function. - other = _convertToTransferFunction(other) + if isinstance(other, (int, float, long, complex)): + other = _convertToTransferFunction(other, inputs=self.inputs, + outputs=self.inputs) + else: + other = _convertToTransferFunction(other) + if (self.inputs > 1 or self.outputs > 1 or other.inputs > 1 or other.outputs > 1): raise NotImplementedError("TransferFunction.__div__ is currently \ @@ -388,6 +392,11 @@ # TODO: Division of MIMO transfer function objects is not written yet. def __rdiv__(self, other): """Right divide two LTI objects.""" + if isinstance(other, (int, float, long, complex)): + other = _convertToTransferFunction(other, inputs=self.inputs, + outputs=self.inputs) + else: + other = _convertToTransferFunction(other) if (self.inputs > 1 or self.outputs > 1 or other.inputs > 1 or other.outputs > 1): @@ -395,6 +404,16 @@ implemented only for SISO systems.") return other / self + def __pow__(self,other): + if not type(other) == int: + raise ValueError("Exponent must be an integer") + if other == 0: + return TransferFunction([1],[1]) #unity + if other > 0: + return self * (self**(other-1)) + if other < 0: + return (TransferFunction([1],[1]) / self) * (self**(other+1)) + def evalfr(self, omega): """Evaluate a transfer function at a single angular frequency. This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: Gustavo G. <gor...@mi...> - 2011-04-17 18:07:48
|
Hey All, I didn't intent to commit the change to setup.py. Sorry about that. I was trying to install "control" like so: $ python setup.py develop The "develop" option makes apparently makes an egg link from the python dist-packages to src folder. This way changes to the src folder take effect in my python environment. You can accomplish the same thing manually with symlinks, I think. Anyway I haven't been able to get this to work. I haven't developed a python module before so I thought I'd ask your methods. Thanks, Gustavo On Sat, Apr 16, 2011 at 7:45 PM, <gor...@us...> wrote: > Revision: 155 > > http://python-control.svn.sourceforge.net/python-control/?rev=155&view=rev > Author: goretkin > Date: 2011-04-16 23:45:46 +0000 (Sat, 16 Apr 2011) > > Log Message: > ----------- > Support for the **,/ operator. > >> s = control.tf.TransferFunction([1, 0],[1]) > sys = s/(s**2+s+1) #works as expected > > Modified Paths: > -------------- > trunk/setup.py > trunk/src/xferfcn.py > > Modified: trunk/setup.py > =================================================================== > --- trunk/setup.py 2011-04-07 20:12:28 UTC (rev 154) > +++ trunk/setup.py 2011-04-16 23:45:46 UTC (rev 155) > @@ -1,6 +1,7 @@ > #!/usr/bin/env python > > -from distutils.core import setup > +#from distutils.core import setup > +from setuptools import setup > > setup(name = 'control', > version = '0.4c', > > Modified: trunk/src/xferfcn.py > =================================================================== > --- trunk/src/xferfcn.py 2011-04-07 20:12:28 UTC (rev 154) > +++ trunk/src/xferfcn.py 2011-04-16 23:45:46 UTC (rev 155) > @@ -372,9 +372,13 @@ > def __div__(self, other): > """Divide two LTI objects.""" > > - # Convert the second argument to a transfer function. > - other = _convertToTransferFunction(other) > + if isinstance(other, (int, float, long, complex)): > + other = _convertToTransferFunction(other, inputs=self.inputs, > + outputs=self.inputs) > + else: > + other = _convertToTransferFunction(other) > > + > if (self.inputs > 1 or self.outputs > 1 or > other.inputs > 1 or other.outputs > 1): > raise NotImplementedError("TransferFunction.__div__ is > currently \ > @@ -388,6 +392,11 @@ > # TODO: Division of MIMO transfer function objects is not written yet. > def __rdiv__(self, other): > """Right divide two LTI objects.""" > + if isinstance(other, (int, float, long, complex)): > + other = _convertToTransferFunction(other, inputs=self.inputs, > + outputs=self.inputs) > + else: > + other = _convertToTransferFunction(other) > > if (self.inputs > 1 or self.outputs > 1 or > other.inputs > 1 or other.outputs > 1): > @@ -395,6 +404,16 @@ > implemented only for SISO systems.") > > return other / self > + def __pow__(self,other): > + if not type(other) == int: > + raise ValueError("Exponent must be an integer") > + if other == 0: > + return TransferFunction([1],[1]) #unity > + if other > 0: > + return self * (self**(other-1)) > + if other < 0: > + return (TransferFunction([1],[1]) / self) * (self**(other+1)) > + > > def evalfr(self, omega): > """Evaluate a transfer function at a single angular frequency. > > > This was sent by the SourceForge.net collaborative development platform, > the world's largest Open Source development site. > > > ------------------------------------------------------------------------------ > Benefiting from Server Virtualization: Beyond Initial Workload > Consolidation -- Increasing the use of server virtualization is a top > priority.Virtualization can reduce costs, simplify management, and improve > application availability and disaster protection. Learn more about boosting > the value of server virtualization. http://p.sf.net/sfu/vmware-sfdev2dev > _______________________________________________ > python-control-discuss mailing list > pyt...@li... > https://lists.sourceforge.net/lists/listinfo/python-control-discuss > |
From: Richard M. <mu...@cd...> - 2011-04-17 18:10:33
|
I've been using symlinks at that works pretty well (and is easy). I'll play with the setup changes and undo if there are problems. Experience from others (distutils vs setuptools) are welcome. -richard On 17 Apr 2011, at 11:07 , Gustavo Goretkin wrote: > Hey All, > > I didn't intent to commit the change to setup.py. Sorry about that. > > I was trying to install "control" like so: > $ python setup.py develop > > The "develop" option makes apparently makes an egg link from the python dist-packages to src folder. This way changes to the src folder take effect in my python environment. You can accomplish the same thing manually with symlinks, I think. > > Anyway I haven't been able to get this to work. I haven't developed a python module before so I thought I'd ask your methods. > > Thanks, > Gustavo > > On Sat, Apr 16, 2011 at 7:45 PM, <gor...@us...> wrote: > Revision: 155 > http://python-control.svn.sourceforge.net/python-control/?rev=155&view=rev > Author: goretkin > Date: 2011-04-16 23:45:46 +0000 (Sat, 16 Apr 2011) > > Log Message: > ----------- > Support for the **,/ operator. > >> s = control.tf.TransferFunction([1, 0],[1]) > sys = s/(s**2+s+1) #works as expected > > Modified Paths: > -------------- > trunk/setup.py > trunk/src/xferfcn.py > > Modified: trunk/setup.py > =================================================================== > --- trunk/setup.py 2011-04-07 20:12:28 UTC (rev 154) > +++ trunk/setup.py 2011-04-16 23:45:46 UTC (rev 155) > @@ -1,6 +1,7 @@ > #!/usr/bin/env python > > -from distutils.core import setup > +#from distutils.core import setup > +from setuptools import setup > > setup(name = 'control', > version = '0.4c', > > Modified: trunk/src/xferfcn.py > =================================================================== > --- trunk/src/xferfcn.py 2011-04-07 20:12:28 UTC (rev 154) > +++ trunk/src/xferfcn.py 2011-04-16 23:45:46 UTC (rev 155) > @@ -372,9 +372,13 @@ > def __div__(self, other): > """Divide two LTI objects.""" > > - # Convert the second argument to a transfer function. > - other = _convertToTransferFunction(other) > + if isinstance(other, (int, float, long, complex)): > + other = _convertToTransferFunction(other, inputs=self.inputs, > + outputs=self.inputs) > + else: > + other = _convertToTransferFunction(other) > > + > if (self.inputs > 1 or self.outputs > 1 or > other.inputs > 1 or other.outputs > 1): > raise NotImplementedError("TransferFunction.__div__ is currently \ > @@ -388,6 +392,11 @@ > # TODO: Division of MIMO transfer function objects is not written yet. > def __rdiv__(self, other): > """Right divide two LTI objects.""" > + if isinstance(other, (int, float, long, complex)): > + other = _convertToTransferFunction(other, inputs=self.inputs, > + outputs=self.inputs) > + else: > + other = _convertToTransferFunction(other) > > if (self.inputs > 1 or self.outputs > 1 or > other.inputs > 1 or other.outputs > 1): > @@ -395,6 +404,16 @@ > implemented only for SISO systems.") > > return other / self > + def __pow__(self,other): > + if not type(other) == int: > + raise ValueError("Exponent must be an integer") > + if other == 0: > + return TransferFunction([1],[1]) #unity > + if other > 0: > + return self * (self**(other-1)) > + if other < 0: > + return (TransferFunction([1],[1]) / self) * (self**(other+1)) > + > > def evalfr(self, omega): > """Evaluate a transfer function at a single angular frequency. > > > This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. > > ------------------------------------------------------------------------------ > Benefiting from Server Virtualization: Beyond Initial Workload > Consolidation -- Increasing the use of server virtualization is a top > priority.Virtualization can reduce costs, simplify management, and improve > application availability and disaster protection. Learn more about boosting > the value of server virtualization. http://p.sf.net/sfu/vmware-sfdev2dev > _______________________________________________ > python-control-discuss mailing list > pyt...@li... > https://lists.sourceforge.net/lists/listinfo/python-control-discuss > > ------------------------------------------------------------------------------ > Benefiting from Server Virtualization: Beyond Initial Workload > Consolidation -- Increasing the use of server virtualization is a top > priority.Virtualization can reduce costs, simplify management, and improve > application availability and disaster protection. Learn more about boosting > the value of server virtualization. http://p.sf.net/sfu/vmware-sfdev2dev_______________________________________________ > python-control-discuss mailing list > pyt...@li... > https://lists.sourceforge.net/lists/listinfo/python-control-discuss |
From: <mur...@us...> - 2011-06-17 23:51:53
|
Revision: 157 http://python-control.svn.sourceforge.net/python-control/?rev=157&view=rev Author: murrayrm Date: 2011-06-17 23:51:46 +0000 (Fri, 17 Jun 2011) Log Message: ----------- Added in matrix equation solver routes (lyap, dlyap, care, dare) from Johnsson, Nordh, Olofsson, Romero). Converted test code to standard format. Modified Paths: -------------- trunk/ChangeLog trunk/src/__init__.py trunk/src/matlab.py Added Paths: ----------- trunk/src/mateqn.py trunk/tests/mateqn_test.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-04-17 18:45:29 UTC (rev 156) +++ trunk/ChangeLog 2011-06-17 23:51:46 UTC (rev 157) @@ -1,3 +1,20 @@ +2011-06-16 Richard Murray <murray@malabar.local> + + * src/matlab.py: import mateqn functions + + * src/__init__.py: import mateqn functions + + * tests/test_all.py: added unit tests for matrix solvers, converting + to standard format along the way. Seems to work even if slycot + routines are not in place, but I'm not sure if this is for the right + reasons... + + * src/mateqn.py: added matrix solvers from LTH (Ola Johnsson, Jerker + Nordh, Bjorn Olofsson, Vanessa Romero). Moved slycot function + checks to the portion of the code where they are used, so that + missing slycot doesn't mess up initialization if proper version of + slycot is not available. + 2011-04-02 Richard Murray <murray@malabar.local> * src/xferfcn.py (TransferFunction.__add__): fixed bug when adding a Modified: trunk/src/__init__.py =================================================================== --- trunk/src/__init__.py 2011-04-17 18:45:29 UTC (rev 156) +++ trunk/src/__init__.py 2011-06-17 23:51:46 UTC (rev 157) @@ -65,3 +65,4 @@ from delay import * from modelsimp import * from rlocus import * +from mateqn import * Added: trunk/src/mateqn.py =================================================================== --- trunk/src/mateqn.py (rev 0) +++ trunk/src/mateqn.py 2011-06-17 23:51:46 UTC (rev 157) @@ -0,0 +1,928 @@ +# mateqn.py - matrix equation solvers (Lyapunov, Riccati) +from __future__ import division + +""" Implementation of the functions lyap, dlyap, care and dare +for solution of Lyapunov and Riccati equations. """ + +"""Copyright (c) 2011, All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the project author nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +Author: Bjorn Olofsson +""" + +from numpy.linalg import inv +from scipy import shape,size,asarray,copy,zeros,eye,dot + +from control.exception import ControlSlycot,ControlArgument + +#### Lyapunov equation solvers lyap and dlyap + +def lyap(A,Q,C=None,E=None): + """ X = lyap(A,Q) solves the continuous-time Lyapunov equation + + A X + X A^T + Q = 0 + + where A and Q are square matrices of the same dimension. + Further, Q must be symmetric. + + X = lyap(A,Q,C) solves the Sylvester equation + + A X + X Q + C = 0 + + where A and Q are square matrices. + + X = lyap(A,Q,None,E) solves the generalized continuous-time + Lyapunov equation + + A X E^T + E X A^T + Q = 0 + + where Q is a symmetric matrix and A, Q and E are square matrices + of the same dimension. """ + + # Make sure we have access to the right slycot routines + try: + from slycot import sb03md + except ImportError: + raise ControlSlycot("can't find slycot module 'sb03md'") + + try: + from slycot import sb04md + except ImportError: + raise ControlSlycot("can't find slycot module 'sb04md'") + + # Reshape 1-d arrays + if len(shape(A)) == 1: + A = A.reshape(1,A.size) + + if len(shape(Q)) == 1: + Q = Q.reshape(1,Q.size) + + if C != None and len(shape(C)) == 1: + C = C.reshape(1,C.size) + + if E != None and len(shape(E)) == 1: + E = E.reshape(1,E.size) + + # Determine main dimensions + if size(A) == 1: + n = 1 + else: + n = size(A,0) + + if size(Q) == 1: + m = 1 + else: + m = size(Q,0) + + # Solve standard Lyapunov equation + if C==None and E==None: + # Check input data for consistency + if shape(A) != shape(Q): + raise ControlArgument("A and Q must be matrices of identical \ + sizes.") + + if size(A) > 1 and shape(A)[0] != shape(A)[1]: + raise ControlArgument("A must be a quadratic matrix.") + + if size(Q) > 1 and shape(Q)[0] != shape(Q)[1]: + raise ControlArgument("Q must be a quadratic matrix.") + + if not (asarray(Q) == asarray(Q).T).all(): + raise ControlArgument("Q must be a symmetric matrix.") + + # Solve the Lyapunov equation by calling Slycot function sb03md + try: + X,scale,sep,ferr,w = sb03md(n,-Q,A,eye(n,n),'C',trana='T') + except ValueError, ve: + if ve.info < 0: + e = ValueError(ve.message) + e.info = ve.info + elif ve.info == n+1: + e = ValueError("The matrix A and -A have common or very \ + close eigenvalues.") + e.info = ve.info + else: + e = ValueError("The QR algorithm failed to compute all \ + the eigenvalues (see LAPACK Library routine DGEES).") + e.info = ve.info + raise e + + # Solve the Sylvester equation + elif C != None and E==None: + # Check input data for consistency + if size(A) > 1 and shape(A)[0] != shape(A)[1]: + raise ControlArgument("A must be a quadratic matrix.") + + if size(Q) > 1 and shape(Q)[0] != shape(Q)[1]: + raise ControlArgument("Q must be a quadratic matrix.") + + if (size(C) > 1 and shape(C)[0] != n) or \ + (size(C) > 1 and shape(C)[1] != m) or \ + (size(C) == 1 and size(A) != 1) or (size(C) == 1 and size(Q) != 1): + raise ControlArgument("C matrix has incompatible dimensions.") + + # Solve the Sylvester equation by calling the Slycot function sb04md + try: + X = sb04md(n,m,A,Q,-C) + except ValueError, ve: + if ve.info < 0: + e = ValueError(ve.message) + e.info = ve.info + elif ve.info > m: + e = ValueError("A singular matrix was encountered whilst \ + solving for the %i-th column of matrix X." % ve.info-m) + e.info = ve.info + else: + e = ValueError("The QR algorithm failed to compute all the \ + eigenvalues (see LAPACK Library routine DGEES).") + e.info = ve.info + raise e + + # Solve the generalized Lyapunov equation + elif C == None and E != None: + # Check input data for consistency + if (size(Q) > 1 and shape(Q)[0] != shape(Q)[1]) or \ + (size(Q) > 1 and shape(Q)[0] != n) or \ + (size(Q) == 1 and n > 1): + raise ControlArgument("Q must be a square matrix with the same \ + dimension as A.") + + if (size(E) > 1 and shape(E)[0] != shape(E)[1]) or \ + (size(E) > 1 and shape(E)[0] != n) or \ + (size(E) == 1 and n > 1): + raise ControlArgument("E must be a square matrix with the same \ + dimension as A.") + + if not (asarray(Q) == asarray(Q).T).all(): + raise ControlArgument("Q must be a symmetric matrix.") + + # Make sure we have access to the write slicot routine + try: + from slycot import sg03ad + except ImportError: + raise ControlSlycot("can't find slycot module 'sg03ad'") + + # Solve the generalized Lyapunov equation by calling Slycot + # function sg03ad + try: + A,E,Q,Z,X,scale,sep,ferr,alphar,alphai,beta = \ + sg03ad('C','B','N','T','L',n,A,E,eye(n,n),eye(n,n),-Q) + except ValueError, ve: + if ve.info < 0 or ve.info > 4: + e = ValueError(ve.message) + e.info = ve.info + elif ve.info == 1: + e = ValueError("The matrix contained in the upper \ + Hessenberg part of the array A is not in \ + upper quasitriangular form") + e.info = ve.info + elif ve.info == 2: + e = ValueError("The pencil A - lambda * E cannot be \ + reduced to generalized Schur form: LAPACK \ + routine DGEGS has failed to converge") + e.info = ve.info + elif ve.info == 4: + e = ValueError("The pencil A - lambda * E has a \ + degenerate pair of eigenvalues. That is, \ + lambda_i = lambda_j for some i and j, where \ + lambda_i and lambda_j are eigenvalues of \ + A - lambda * E. Hence, the equation is \ + singular; perturbed values were \ + used to solve the equation (but the matrices \ + A and E are unchanged)") + e.info = ve.info + raise e + # Invalid set of input parameters + else: + raise ControlArgument("Invalid set of input parameters") + + return X + + +def dlyap(A,Q,C=None,E=None): + """ dlyap(A,Q) solves the discrete-time Lyapunov equation + + A X A^T - X + Q = 0 + + where A and Q are square matrices of the same dimension. Further + Q must be symmetric. + + dlyap(A,Q,C) solves the Sylvester equation + + A X Q^T - X + C = 0 + + where A and Q are square matrices. + + dlyap(A,Q,None,E) solves the generalized discrete-time Lyapunov + equation + + A X A^T - E X E^T + Q = 0 + + where Q is a symmetric matrix and A, Q and E are square matrices + of the same dimension. """ + + # Make sure we have access to the right slycot routines + try: + from slycot import sb03md + except ImportError: + raise ControlSlycot("can't find slycot module 'sb03md'") + + try: + from slycot import sb04qd + except ImportError: + raise ControlSlycot("can't find slycot module 'sb04qd'") + + try: + from slycot import sg03ad + except ImportError: + raise ControlSlycot("can't find slycot module 'sg03ad'") + + # Reshape 1-d arrays + if len(shape(A)) == 1: + A = A.reshape(1,A.size) + + if len(shape(Q)) == 1: + Q = Q.reshape(1,Q.size) + + if C != None and len(shape(C)) == 1: + C = C.reshape(1,C.size) + + if E != None and len(shape(E)) == 1: + E = E.reshape(1,E.size) + + # Determine main dimensions + if size(A) == 1: + n = 1 + else: + n = size(A,0) + + if size(Q) == 1: + m = 1 + else: + m = size(Q,0) + + # Solve standard Lyapunov equation + if C==None and E==None: + # Check input data for consistency + if shape(A) != shape(Q): + raise ControlArgument("A and Q must be matrices of identical \ + sizes.") + + if size(A) > 1 and shape(A)[0] != shape(A)[1]: + raise ControlArgument("A must be a quadratic matrix.") + + if size(Q) > 1 and shape(Q)[0] != shape(Q)[1]: + raise ControlArgument("Q must be a quadratic matrix.") + + if not (asarray(Q) == asarray(Q).T).all(): + raise ControlArgument("Q must be a symmetric matrix.") + + # Solve the Lyapunov equation by calling the Slycot function sb03md + try: + X,scale,sep,ferr,w = sb03md(n,-Q,A,eye(n,n),'D',trana='T') + except ValueError, ve: + if ve.info < 0: + e = ValueError(ve.message) + e.info = ve.info + else: + e = ValueError("The QR algorithm failed to compute all the \ + eigenvalues (see LAPACK Library routine DGEES).") + e.info = ve.info + raise e + + # Solve the Sylvester equation + elif C != None and E==None: + # Check input data for consistency + if size(A) > 1 and shape(A)[0] != shape(A)[1]: + raise ControlArgument("A must be a quadratic matrix") + + if size(Q) > 1 and shape(Q)[0] != shape(Q)[1]: + raise ControlArgument("Q must be a quadratic matrix") + + if (size(C) > 1 and shape(C)[0] != n) or \ + (size(C) > 1 and shape(C)[1] != m) or \ + (size(C) == 1 and size(A) != 1) or (size(C) == 1 and size(Q) != 1): + raise ControlArgument("C matrix has incompatible dimensions") + + # Solve the Sylvester equation by calling Slycot function sb04qd + try: + X = sb04qd(n,m,-A,asarray(Q).T,C) + except ValueError, ve: + if ve.info < 0: + e = ValueError(ve.message) + e.info = ve.info + elif ve.info > m: + e = ValueError("A singular matrix was encountered whilst \ + solving for the %i-th column of matrix X." % ve.info-m) + e.info = ve.info + else: + e = ValueError("The QR algorithm failed to compute all the \ + eigenvalues (see LAPACK Library routine DGEES)") + e.info = ve.info + raise e + + # Solve the generalized Lyapunov equation + elif C == None and E != None: + # Check input data for consistency + if (size(Q) > 1 and shape(Q)[0] != shape(Q)[1]) or \ + (size(Q) > 1 and shape(Q)[0] != n) or \ + (size(Q) == 1 and n > 1): + raise ControlArgument("Q must be a square matrix with the same \ + dimension as A.") + + if (size(E) > 1 and shape(E)[0] != shape(E)[1]) or \ + (size(E) > 1 and shape(E)[0] != n) or \ + (size(E) == 1 and n > 1): + raise ControlArgument("E must be a square matrix with the same \ + dimension as A.") + + if not (asarray(Q) == asarray(Q).T).all(): + raise ControlArgument("Q must be a symmetric matrix.") + + # Solve the generalized Lyapunov equation by calling Slycot + # function sg03ad + try: + A,E,Q,Z,X,scale,sep,ferr,alphar,alphai,beta = \ + sg03ad('D','B','N','T','L',n,A,E,eye(n,n),eye(n,n),-Q) + except ValueError, ve: + if ve.info < 0 or ve.info > 4: + e = ValueError(ve.message) + e.info = ve.info + elif ve.info == 1: + e = ValueError("The matrix contained in the upper \ + Hessenberg part of the array A is not in \ + upper quasitriangular form") + e.info = ve.info + elif ve.info == 2: + e = ValueError("The pencil A - lambda * E cannot be \ + reduced to generalized Schur form: LAPACK \ + routine DGEGS has failed to converge") + e.info = ve.info + elif ve.info == 3: + e = ValueError("The pencil A - lambda * E has a \ + pair of reciprocal eigenvalues. That is, \ + lambda_i = 1/lambda_j for some i and j, \ + where lambda_i and lambda_j are eigenvalues \ + of A - lambda * E. Hence, the equation is \ + singular; perturbed values were \ + used to solve the equation (but the \ + matrices A and E are unchanged)") + e.info = ve.info + raise e + # Invalid set of input parameters + else: + raise ControlArgument("Invalid set of input parameters") + + return X + + + +#### Riccati equation solvers care and dare + +def care(A,B,Q,R=None,S=None,E=None): + """ (X,L,G) = care(A,B,Q) solves the continuous-time algebraic Riccati + equation + + A^T X + X A - X B B^T X + Q = 0 + + where A and Q are square matrices of the same dimension. Further, Q + is a symmetric matrix. The function returns the solution X, the gain + matrix G = B^T X and the closed loop eigenvalues L, i.e., the eigenvalues + of A - B G. + + (X,L,G) = care(A,B,Q,R,S,E) solves the generalized continuous-time + algebraic Riccati equation + + A^T X E + E^T X A - (E^T X B + S) R^-1 (B^T X E + S^T) + Q = 0 + + where A, Q and E are square matrices of the same dimension. Further, Q and + R are symmetric matrices. The function returns the solution X, the gain + matrix G = R^-1 (B^T X E + S^T) and the closed loop eigenvalues L, i.e., + the eigenvalues of A - B G , E. """ + + # Make sure we can import required slycot routine + try: + from slycot import sb02md + except ImportError: + raise ControlSlycot("can't find slycot module 'sb02md'") + + try: + from slycot import sb02mt + except ImportError: + raise ControlSlycot("can't find slycot module 'sb02mt'") + + # Make sure we can find the required slycot routine + try: + from slycot import sg02ad + except ImportError: + raise ControlSlycot("can't find slycot module 'sg02ad'") + + # Reshape 1-d arrays + if len(shape(A)) == 1: + A = A.reshape(1,A.size) + + if len(shape(B)) == 1: + B = B.reshape(1,B.size) + + if len(shape(Q)) == 1: + Q = Q.reshape(1,Q.size) + + if R != None and len(shape(R)) == 1: + R = R.reshape(1,R.size) + + if S != None and len(shape(S)) == 1: + S = S.reshape(1,S.size) + + if E != None and len(shape(E)) == 1: + E = E.reshape(1,E.size) + + # Determine main dimensions + if size(A) == 1: + n = 1 + else: + n = size(A,0) + + if size(B) == 1: + m = 1 + else: + m = size(B,1) + if R==None: + R = eye(m,m) + + # Solve the standard algebraic Riccati equation + if S==None and E==None: + # Check input data for consistency + if size(A) > 1 and shape(A)[0] != shape(A)[1]: + raise ControlArgument("A must be a quadratic matrix.") + + if (size(Q) > 1 and shape(Q)[0] != shape(Q)[1]) or \ + (size(Q) > 1 and shape(Q)[0] != n) or \ + size(Q) == 1 and n > 1: + raise ControlArgument("Q must be a quadratic matrix of the same \ + dimension as A.") + + if (size(B) > 1 and shape(B)[0] != n) or \ + size(B) == 1 and n > 1: + raise ControlArgument("Incompatible dimensions of B matrix.") + + if not (asarray(Q) == asarray(Q).T).all(): + raise ControlArgument("Q must be a symmetric matrix.") + + if not (asarray(R) == asarray(R).T).all(): + raise ControlArgument("R must be a symmetric matrix.") + + # Create back-up of arrays needed for later computations + R_ba = copy(R) + B_ba = copy(B) + + # Solve the standard algebraic Riccati equation by calling Slycot + # functions sb02mt and sb02md + try: + A_b,B_b,Q_b,R_b,L_b,ipiv,oufact,G = sb02mt(n,m,B,R) + except ValueError, ve: + if ve.info < 0: + e = ValueError(ve.message) + e.info = ve.info + elif ve.info == m+1: + e = ValueError("The matrix R is numerically singular.") + e.info = ve.info + else: + e = ValueError("The %i-th element of d in the UdU (LdL) \ + factorization is zero." % ve.info) + e.info = ve.info + raise e + + try: + X,rcond,w,S_o,U,A_inv = sb02md(n,A,G,Q,'C') + except ValueError, ve: + if ve.info < 0 or ve.info > 5: + e = ValueError(ve.message) + e.info = ve.info + elif ve.info == 1: + e = ValueError("The matrix A is (numerically) singular in \ + discrete-time case.") + e.info = ve.info + elif ve.info == 2: + e = ValueError("The Hamiltonian or symplectic matrix H cannot \ + be reduced to real Schur form.") + e.info = ve.info + elif ve.info == 3: + e = ValueError("The real Schur form of the Hamiltonian or \ + symplectic matrix H cannot be appropriately ordered.") + e.info = ve.info + elif ve.info == 4: + e = ValueError("The Hamiltonian or symplectic matrix H has \ + less than n stable eigenvalues.") + e.info = ve.info + elif ve.info == 5: + e = ValueError("The N-th order system of linear algebraic \ + equations is singular to working precision.") + e.info = ve.info + raise e + + # Calculate the gain matrix G + if size(R_b) == 1: + G = dot(dot(1/(R_ba),asarray(B_ba).T) , X) + else: + G = dot(dot(inv(R_ba),asarray(B_ba).T) , X) + + # Return the solution X, the closed-loop eigenvalues L and + # the gain matrix G + return (X , w[:n] , G ) + + # Solve the generalized algebraic Riccati equation + elif S != None and E != None: + # Check input data for consistency + if size(A) > 1 and shape(A)[0] != shape(A)[1]: + raise ControlArgument("A must be a quadratic matrix.") + + if (size(Q) > 1 and shape(Q)[0] != shape(Q)[1]) or \ + (size(Q) > 1 and shape(Q)[0] != n) or \ + size(Q) == 1 and n > 1: + raise ControlArgument("Q must be a quadratic matrix of the same \ + dimension as A.") + + if (size(B) > 1 and shape(B)[0] != n) or \ + size(B) == 1 and n > 1: + raise ControlArgument("Incompatible dimensions of B matrix.") + + if (size(E) > 1 and shape(E)[0] != shape(E)[1]) or \ + (size(E) > 1 and shape(E)[0] != n) or \ + size(E) == 1 and n > 1: + raise ControlArgument("E must be a quadratic matrix of the same \ + dimension as A.") + + if (size(R) > 1 and shape(R)[0] != shape(R)[1]) or \ + (size(R) > 1 and shape(R)[0] != m) or \ + size(R) == 1 and m > 1: + raise ControlArgument("R must be a quadratic matrix of the same \ + dimension as the number of columns in the B matrix.") + + if (size(S) > 1 and shape(S)[0] != n) or \ + (size(S) > 1 and shape(S)[1] != m) or \ + size(S) == 1 and n > 1 or \ + size(S) == 1 and m > 1: + raise ControlArgument("Incompatible dimensions of S matrix.") + + if not (asarray(Q) == asarray(Q).T).all(): + raise ControlArgument("Q must be a symmetric matrix.") + + if not (asarray(R) == asarray(R).T).all(): + raise ControlArgument("R must be a symmetric matrix.") + + # Create back-up of arrays needed for later computations + R_b = copy(R) + B_b = copy(B) + E_b = copy(E) + S_b = copy(S) + + # Solve the generalized algebraic Riccati equation by calling the + # Slycot function sg02ad + try: + rcondu,X,alfar,alfai,beta,S_o,T,U,iwarn = \ + sg02ad('C','B','N','U','N','N','S','R',n,m,0,A,E,B,Q,R,S) + except ValueError, ve: + if ve.info < 0 or ve.info > 7: + e = ValueError(ve.message) + e.info = ve.info + elif ve.info == 1: + e = ValueError("The computed extended matrix pencil is \ + singular, possibly due to rounding errors.") + e.info = ve.info + elif ve.info == 2: + e = ValueError("The QZ algorithm failed.") + e.info = ve.info + elif ve.info == 3: + e = ValueError("Reordering of the generalized eigenvalues \ + failed.") + e.info = ve.info + elif ve.info == 4: + e = ValueError("After reordering, roundoff changed values of \ + some complex eigenvalues so that leading \ + eigenvalues in the generalized Schur form no \ + longer satisfy the stability condition; this \ + could also be caused due to scaling.") + e.info = ve.info + elif ve.info == 5: + e = ValueError("The computed dimension of the solution does \ + not equal N.") + e.info = ve.info + elif ve.info == 6: + e = ValueError("The spectrum is too close to the boundary of \ + the stability domain.") + e.info = ve.info + elif ve.info == 7: + e = ValueError("A singular matrix was encountered during the \ + computation of the solution matrix X.") + e.info = ve.info + raise e + + # Calculate the closed-loop eigenvalues L + L = zeros((n,1)) + L.dtype = 'complex64' + for i in range(n): + L[i] = (alfar[i] + alfai[i]*1j)/beta[i] + + # Calculate the gain matrix G + if size(R_b) == 1: + G = dot(1/(R_b),dot(asarray(B_b).T,dot(X,E_b))+asarray(S_b).T) + else: + G = dot(inv(R_b),dot(asarray(B_b).T,dot(X,E_b))+asarray(S_b).T) + + # Return the solution X, the closed-loop eigenvalues L and + # the gain matrix G + return (X , L , G) + + # Invalid set of input parameters + else: + raise ControlArgument("Invalid set of input parameters.") + + +def dare(A,B,Q,R,S=None,E=None): + """ (X,L,G) = dare(A,B,Q,R) solves the discrete-time algebraic Riccati + equation + + A^T X A - X - A^T X B (B^T X B + R)^-1 B^T X A + Q = 0 + + where A and Q are square matrices of the same dimension. Further, Q + is a symmetric matrix. The function returns the solution X, the gain + matrix G = (B^T X B + R)^-1 B^T X A and the closed loop eigenvalues L, + i.e., the eigenvalues of A - B G. + + (X,L,G) = dare(A,B,Q,R,S,E) solves the generalized discrete-time algebraic + Riccati equation + + A^T X A - E^T X E - (A^T X B + S) (B^T X B + R)^-1 (B^T X A + S^T) + + + Q = 0 + + where A, Q and E are square matrices of the same dimension. Further, Q and + R are symmetric matrices. The function returns the solution X, the gain + matrix G = (B^T X B + R)^-1 (B^T X A + S^T) and the closed loop + eigenvalues L, i.e., the eigenvalues of A - B G , E. """ + + # Make sure we can import required slycot routine + try: + from slycot import sb02md + except ImportError: + raise ControlSlycot("can't find slycot module 'sb02md'") + + try: + from slycot import sb02mt + except ImportError: + raise ControlSlycot("can't find slycot module 'sb02mt'") + + # Make sure we can find the required slycot routine + try: + from slycot import sg02ad + except ImportError: + raise ControlSlycot("can't find slycot module 'sg02ad'") + + # Reshape 1-d arrays + if len(shape(A)) == 1: + A = A.reshape(1,A.size) + + if len(shape(B)) == 1: + B = B.reshape(1,B.size) + + if len(shape(Q)) == 1: + Q = Q.reshape(1,Q.size) + + if R != None and len(shape(R)) == 1: + R = R.reshape(1,R.size) + + if S != None and len(shape(S)) == 1: + S = S.reshape(1,S.size) + + if E != None and len(shape(E)) == 1: + E = E.reshape(1,E.size) + + # Determine main dimensions + if size(A) == 1: + n = 1 + else: + n = size(A,0) + + if size(B) == 1: + m = 1 + else: + m = size(B,1) + + # Solve the standard algebraic Riccati equation + if S==None and E==None: + # Check input data for consistency + if size(A) > 1 and shape(A)[0] != shape(A)[1]: + raise ControlArgument("A must be a quadratic matrix.") + + if (size(Q) > 1 and shape(Q)[0] != shape(Q)[1]) or \ + (size(Q) > 1 and shape(Q)[0] != n) or \ + size(Q) == 1 and n > 1: + raise ControlArgument("Q must be a quadratic matrix of the same \ + dimension as A.") + + if (size(B) > 1 and shape(B)[0] != n) or \ + size(B) == 1 and n > 1: + raise ControlArgument("Incompatible dimensions of B matrix.") + + if not (asarray(Q) == asarray(Q).T).all(): + raise ControlArgument("Q must be a symmetric matrix.") + + if not (asarray(R) == asarray(R).T).all(): + raise ControlArgument("R must be a symmetric matrix.") + + # Create back-up of arrays needed for later computations + A_ba = copy(A) + R_ba = copy(R) + B_ba = copy(B) + + # Solve the standard algebraic Riccati equation by calling Slycot + # functions sb02mt and sb02md + try: + A_b,B_b,Q_b,R_b,L_b,ipiv,oufact,G = sb02mt(n,m,B,R) + except ValueError, ve: + if ve.info < 0: + e = ValueError(ve.message) + e.info = ve.info + elif ve.info == m+1: + e = ValueError("The matrix R is numerically singular.") + e.info = ve.info + else: + e = ValueError("The %i-th element of d in the UdU (LdL) \ + factorization is zero." % ve.info) + e.info = ve.info + raise e + + try: + X,rcond,w,S,U,A_inv = sb02md(n,A,G,Q,'D') + except ValueError, ve: + if ve.info < 0 or ve.info > 5: + e = ValueError(ve.message) + e.info = ve.info + elif ve.info == 1: + e = ValueError("The matrix A is (numerically) singular in \ + discrete-time case.") + e.info = ve.info + elif ve.info == 2: + e = ValueError("The Hamiltonian or symplectic matrix H cannot \ + be reduced to real Schur form.") + e.info = ve.info + elif ve.info == 3: + e = ValueError("The real Schur form of the Hamiltonian or \ + symplectic matrix H cannot be appropriately ordered.") + e.info = ve.info + elif ve.info == 4: + e = ValueError("The Hamiltonian or symplectic matrix H has \ + less than n stable eigenvalues.") + e.info = ve.info + elif ve.info == 5: + e = ValueError("The N-th order system of linear algebraic \ + equations is singular to working precision.") + e.info = ve.info + raise e + + # Calculate the gain matrix G + if size(R_b) == 1: + G = dot( 1/(dot(asarray(B_ba).T,dot(X,B_ba))+R_ba) , \ + dot(asarray(B_ba).T,dot(X,A_ba)) ) + else: + G = dot( inv(dot(asarray(B_ba).T,dot(X,B_ba))+R_ba) , \ + dot(asarray(B_ba).T,dot(X,A_ba)) ) + + # Return the solution X, the closed-loop eigenvalues L and + # the gain matrix G + return (X , w[:n] , G) + + # Solve the generalized algebraic Riccati equation + elif S != None and E != None: + # Check input data for consistency + if size(A) > 1 and shape(A)[0] != shape(A)[1]: + raise ControlArgument("A must be a quadratic matrix.") + + if (size(Q) > 1 and shape(Q)[0] != shape(Q)[1]) or \ + (size(Q) > 1 and shape(Q)[0] != n) or \ + size(Q) == 1 and n > 1: + raise ControlArgument("Q must be a quadratic matrix of the same \ + dimension as A.") + + if (size(B) > 1 and shape(B)[0] != n) or \ + size(B) == 1 and n > 1: + raise ControlArgument("Incompatible dimensions of B matrix.") + + if (size(E) > 1 and shape(E)[0] != shape(E)[1]) or \ + (size(E) > 1 and shape(E)[0] != n) or \ + size(E) == 1 and n > 1: + raise ControlArgument("E must be a quadratic matrix of the same \ + dimension as A.") + + if (size(R) > 1 and shape(R)[0] != shape(R)[1]) or \ + (size(R) > 1 and shape(R)[0] != m) or \ + size(R) == 1 and m > 1: + raise ControlArgument("R must be a quadratic matrix of the same \ + dimension as the number of columns in the B matrix.") + + if (size(S) > 1 and shape(S)[0] != n) or \ + (size(S) > 1 and shape(S)[1] != m) or \ + size(S) == 1 and n > 1 or \ + size(S) == 1 and m > 1: + raise ControlArgument("Incompatible dimensions of S matrix.") + + if not (asarray(Q) == asarray(Q).T).all(): + raise ControlArgument("Q must be a symmetric matrix.") + + if not (asarray(R) == asarray(R).T).all(): + raise ControlArgument("R must be a symmetric matrix.") + + # Create back-up of arrays needed for later computations + A_b = copy(A) + R_b = copy(R) + B_b = copy(B) + E_b = copy(E) + S_b = copy(S) + + # Solve the generalized algebraic Riccati equation by calling the + # Slycot function sg02ad + try: + rcondu,X,alfar,alfai,beta,S_o,T,U,iwarn = \ + sg02ad('D','B','N','U','N','N','S','R',n,m,0,A,E,B,Q,R,S) + except ValueError, ve: + if ve.info < 0 or ve.info > 7: + e = ValueError(ve.message) + e.info = ve.info + elif ve.info == 1: + e = ValueError("The computed extended matrix pencil is \ + singular, possibly due to rounding errors.") + e.info = ve.info + elif ve.info == 2: + e = ValueError("The QZ algorithm failed.") + e.info = ve.info + elif ve.info == 3: + e = ValueError("Reordering of the generalized eigenvalues \ + failed.") + e.info = ve.info + elif ve.info == 4: + e = ValueError("After reordering, roundoff changed values of \ + some complex eigenvalues so that leading \ + eigenvalues in the generalized Schur form no \ + longer satisfy the stability condition; this \ + could also be caused due to scaling.") + e.info = ve.info + elif ve.info == 5: + e = ValueError("The computed dimension of the solution does \ + not equal N.") + e.info = ve.info + elif ve.info == 6: + e = ValueError("The spectrum is too close to the boundary of \ + the stability domain.") + e.info = ve.info + elif ve.info == 7: + e = ValueError("A singular matrix was encountered during the \ + computation of the solution matrix X.") + e.info = ve.info + raise e + + L = zeros((n,1)) + L.dtype = 'complex64' + for i in range(n): + L[i] = (alfar[i] + alfai[i]*1j)/beta[i] + + # Calculate the gain matrix G + if size(R_b) == 1: + G = dot( 1/(dot(asarray(B_b).T,dot(X,B_b))+R_b) , \ + dot(asarray(B_b).T,dot(X,A_b)) + asarray(S_b).T) + else: + G = dot( inv(dot(asarray(B_b).T,dot(X,B_b))+R_b) , \ + dot(asarray(B_b).T,dot(X,A_b)) + asarray(S_b).T) + + # Return the solution X, the closed-loop eigenvalues L and + # the gain matrix G + return (X , L , G) + + # Invalid set of input parameters + else: + raise ControlArgument("Invalid set of input parameters.") Modified: trunk/src/matlab.py =================================================================== --- trunk/src/matlab.py 2011-04-17 18:45:29 UTC (rev 156) +++ trunk/src/matlab.py 2011-06-17 23:51:46 UTC (rev 157) @@ -81,6 +81,7 @@ from statefbk import ctrb, obsv, gram, place, lqr from delay import pade from modelsimp import hsvd, balred, modred +from mateqn import lyap, dlyap, dare, care __doc__ += """ The control.matlab module defines functions that are roughly the @@ -255,9 +256,9 @@ lti/conj - complex conjugation of model coefficients Matrix equation solvers and linear algebra - lyap, dlyap - solve Lyapunov equations +\* lyap, dlyap - solve Lyapunov equations lyapchol, dlyapchol - square-root Lyapunov solvers - care, dare - solve algebraic Riccati equations +\* care, dare - solve algebraic Riccati equations gcare, gdare - generalized Riccati solvers bdschur - block diagonalization of a square matrix Added: trunk/tests/mateqn_test.py =================================================================== --- trunk/tests/mateqn_test.py (rev 0) +++ trunk/tests/mateqn_test.py 2011-06-17 23:51:46 UTC (rev 157) @@ -0,0 +1,236 @@ +#!/usr/bin/env python +# +# mateqn_test.py - test wuit for matrix equation solvers +# +#! Currently uses numpy.testing framework; will dump you out of unittest +#! if an error occurs. Should figure out the right way to fix this. + +""" Test cases for lyap, dlyap, care and dare functions in the file +pyctrl_lin_alg.py. """ + +"""Copyright (c) 2011, All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the project author nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +Author: Bjorn Olofsson +""" + +import unittest +from numpy import array +from numpy.testing import assert_array_almost_equal +from numpy.linalg import inv +from scipy import zeros,dot +from control.mateqn import lyap,dlyap,care,dare + +# Test cases (lyap and dlyap) + +class TestMatrixEquations(unittest.TestCase): + """These are tests for the matrix equation solvers in mateqn.py""" + + def setUp(self): + # Make sure that the slycot functions that we require exist + from slycot import sb02md, sb02mt, sb03md, sb04md, sg02ad, \ + sg03ad, sb04qd + + def test_lyap(self): + A = array([[-1, 1],[-1, 0]]) + Q = array([[1,0],[0,1]]) + X = lyap(A,Q) + # print "The solution obtained is ", X + assert_array_almost_equal(dot(A,X)+dot(X,A.T)+Q,zeros((2,2))) + + A = array([[1, 2],[-3, -4]]) + Q = array([[3, 1],[1, 1]]) + X = lyap(A,Q) + # print "The solution obtained is ", X + assert_array_almost_equal(dot(A,X)+dot(X,A.T)+Q,zeros((2,2))) + + def test_lyap_sylvester(self): + A = 5 + B = array([[4, 3], [4, 3]]) + C = array([2, 1]) + X = lyap(A,B,C) + # print "The solution obtained is ", X + assert_array_almost_equal(dot(A,X)+dot(X,B)+C,zeros((1,2))) + + A = array([[2,1],[1,2]]) + B = array([[1,2],[0.5,0.1]]) + C = array([[1,0],[0,1]]) + X = lyap(A,B,C) + # print "The solution obtained is ", X + assert_array_almost_equal(dot(A,X)+dot(X,B)+C,zeros((2,2))) + + def test_lyap_g(self): + A = array([[-1, 2],[-3, -4]]) + Q = array([[3, 1],[1, 1]]) + E = array([[1,2],[2,1]]) + X = lyap(A,Q,None,E) + # print "The solution obtained is ", X + assert_array_almost_equal(dot(A,dot(X,E.T)) + dot(E,dot(X,A.T)) + Q, \ + zeros((2,2))) + + def test_dlyap(self): + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[1,0],[0,1]]) + X = dlyap(A,Q) + # print "The solution obtained is ", X + assert_array_almost_equal(dot(A,dot(X,A.T))-X+Q,zeros((2,2))) + + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[3, 1],[1, 1]]) + X = dlyap(A,Q) + # print "The solution obtained is ", X + assert_array_almost_equal(dot(A,dot(X,A.T))-X+Q,zeros((2,2))) + + def test_dlyap_g(self): + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[3, 1],[1, 1]]) + E = array([[1, 1],[2, 1]]) + X = dlyap(A,Q,None,E) + # print "The solution obtained is ", X + assert_array_almost_equal(dot(A,dot(X,A.T))-dot(E,dot(X,E.T))+Q, \ + zeros((2,2))) + + def test_dlyap_sylvester(self): + A = 5 + B = array([[4, 3], [4, 3]]) + C = array([2, 1]) + X = dlyap(A,B,C) + # print "The solution obtained is ", X + assert_array_almost_equal(dot(A,dot(X,B.T))-X+C,zeros((1,2))) + + A = array([[2,1],[1,2]]) + B = array([[1,2],[0.5,0.1]]) + C = array([[1,0],[0,1]]) + X = dlyap(A,B,C) + # print "The solution obtained is ", X + assert_array_almost_equal(dot(A,dot(X,B.T))-X+C,zeros((2,2))) + + def test_care(self): + A = array([[-2, -1],[-1, -1]]) + Q = array([[0, 0],[0, 1]]) + B = array([[1, 0],[0, 4]]) + + X,L,G = care(A,B,Q) + # print "The solution obtained is", X + assert_array_almost_equal(dot(A.T,X) + dot(X,A) - \ + dot(X,dot(B,dot(B.T,X))) + Q , zeros((2,2))) + assert_array_almost_equal(dot(B.T,X) , G) + + def test_care_g(self): + A = array([[-2, -1],[-1, -1]]) + Q = array([[0, 0],[0, 1]]) + B = array([[1, 0],[0, 4]]) + R = array([[2, 0],[0, 1]]) + S = array([[0, 0],[0, 0]]) + E = array([[2, 1],[1, 2]]) + + X,L,G = care(A,B,Q,R,S,E) + # print "The solution obtained is", X + assert_array_almost_equal(dot(A.T,dot(X,E)) + dot(E.T,dot(X,A)) - \ + dot(dot(dot(E.T,dot(X,B))+S,inv(R) ) , + dot(B.T,dot(X,E))+S.T ) + Q , \ + zeros((2,2))) + assert_array_almost_equal(dot( inv(R) , dot(B.T,dot(X,E)) + S.T) , G) + + A = array([[-2, -1],[-1, -1]]) + Q = array([[0, 0],[0, 1]]) + B = array([[1],[0]]) + R = 1 + S = array([[1],[0]]) + E = array([[2, 1],[1, 2]]) + + X,L,G = care(A,B,Q,R,S,E) + # print "The solution obtained is", X + assert_array_almost_equal(dot(A.T,dot(X,E)) + dot(E.T,dot(X,A)) - \ + dot( dot( dot(E.T,dot(X,B))+S,1/R ) , dot(B.T,dot(X,E))+S.T ) \ + + Q , zeros((2,2))) + assert_array_almost_equal(dot( 1/R , dot(B.T,dot(X,E)) + S.T) , G) + + def test_dare(self): + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[2, 1],[1, 0]]) + B = array([[2, 1],[0, 1]]) + R = array([[1, 0],[0, 1]]) + + X,L,G = dare(A,B,Q,R) + # print "The solution obtained is", X + assert_array_almost_equal(dot(A.T,dot(X,A))-X-dot(dot(dot(A.T,dot(X,B)) , \ + inv(dot(B.T,dot(X,B))+R)) , dot(B.T,dot(X,A))) + Q , zeros((2,2)) ) + assert_array_almost_equal( dot( inv( dot(B.T,dot(X,B)) + R) , \ + dot(B.T,dot(X,A)) ) , G) + + A = array([[1, 0],[-1, 1]]) + Q = array([[0, 1],[1, 1]]) + B = array([[1],[0]]) + R = 2 + + X,L,G = dare(A,B,Q,R) + # print "The solution obtained is", X + assert_array_almost_equal(dot(A.T,dot(X,A))-X-dot(dot(dot(A.T,dot(X,B)) , \ + inv(dot(B.T,dot(X,B))+R)) , dot(B.T,dot(X,A))) + Q , zeros((2,2)) ) + assert_array_almost_equal( dot( 1 / ( dot(B.T,dot(X,B)) + R) , \ + dot(B.T,dot(X,A)) ) , G) + + def test_dare_g(self): + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[2, 1],[1, 3]]) + B = array([[1, 5],[2, 4]]) + R = array([[1, 0],[0, 1]]) + S = array([[1, 0],[2, 0]]) + E = array([[2, 1],[1, 2]]) + + X,L,G = dare(A,B,Q,R,S,E) + # print "The solution obtained is", X + assert_array_almost_equal(dot(A.T,dot(X,A))-dot(E.T,dot(X,E)) - \ + dot( dot(A.T,dot(X,B))+S , dot( inv(dot(B.T,dot(X,B)) + R) , + dot(B.T,dot(X,A))+S.T)) + Q , zeros((2,2)) ) + assert_array_almost_equal( dot( inv( dot(B.T,dot(X,B)) + R) , \ + dot(B.T,dot(X,A)) + S.T ) , G) + + A = array([[-0.6, 0],[-0.1, -0.4]]) + Q = array([[2, 1],[1, 3]]) + B = array([[1],[2]]) + R = 1 + S = array([[1],[2]]) + E = array([[2, 1],[1, 2]]) + + X,L,G = dare(A,B,Q,R,S,E) + # print "The solution obtained is", X + assert_array_almost_equal(dot(A.T,dot(X,A))-dot(E.T,dot(X,E)) - \ + dot( dot(A.T,dot(X,B))+S , dot( inv(dot(B.T,dot(X,B)) + R) , + dot(B.T,dot(X,A))+S.T)) + Q , zeros((2,2)) ) + assert_array_almost_equal( dot( 1 / ( dot(B.T,dot(X,B)) + R) , \ + dot(B.T,dot(X,A)) + S.T ) , G) + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestMatrixEquations) + +if __name__ == "__main__": + unittest.main() This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2011-06-18 00:20:55
|
Revision: 160 http://python-control.svn.sourceforge.net/python-control/?rev=160&view=rev Author: murrayrm Date: 2011-06-18 00:20:49 +0000 (Sat, 18 Jun 2011) Log Message: ----------- updated working version number to 0.4d Modified Paths: -------------- trunk/ChangeLog trunk/setup.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-06-18 00:18:23 UTC (rev 159) +++ trunk/ChangeLog 2011-06-18 00:20:49 UTC (rev 160) @@ -1,3 +1,12 @@ +---- control-0.4c released ----- + +2011-06-17 Richard Murray <mu...@dh...> + + * examples/tfvis.py: Added tfvis, Simple GUI application for + visualizing how the poles/zeros of the transfer function effects the + bode, nyquist and step response of a SISO system. Contributed by + Vanessa Romero Segovia, Ola Johnsson, Jerker Nordh. + 2011-06-16 Richard Murray <murray@malabar.local> * src/matlab.py: import mateqn functions Modified: trunk/setup.py =================================================================== --- trunk/setup.py 2011-06-18 00:18:23 UTC (rev 159) +++ trunk/setup.py 2011-06-18 00:20:49 UTC (rev 160) @@ -4,7 +4,7 @@ from setuptools import setup setup(name = 'control', - version = '0.4c', + version = '0.4d', description = 'Python Control Systems Library', author = 'Richard Murray', author_email = 'mu...@cd...', This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2011-06-18 19:13:17
|
Revision: 161 http://python-control.svn.sourceforge.net/python-control/?rev=161&view=rev Author: murrayrm Date: 2011-06-18 19:13:09 +0000 (Sat, 18 Jun 2011) Log Message: ----------- Added Eike's Welk's time response functions, including MIMO capability. To maintain MATLAB compatibility, the underlying functions have been moved to a new module, timeresp, and renamed ForcedResponse (lsim), InitialResponse (initial), ImpulseResponse (impulse), StepResponse (step). MATLAB version of the functions (with the usual names: lsim, impulse, initial, step) use MATLAB conventions for time vectors. See ChangeLog for detailed list of changes. Modified Paths: -------------- trunk/ChangeLog trunk/doc/conf.py trunk/examples/pvtol-lqr.py trunk/examples/pvtol-nested-ss.py trunk/examples/pvtol-nested.py trunk/src/__init__.py trunk/src/matlab.py trunk/src/statesp.py trunk/tests/matlab_test.py Added Paths: ----------- trunk/src/timeresp.py trunk/tests/test_control_matlab.py trunk/tests/timeresp_test.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-06-18 00:20:49 UTC (rev 160) +++ trunk/ChangeLog 2011-06-18 19:13:09 UTC (rev 161) @@ -1,3 +1,66 @@ +2011-06-18 Richard Murray <murray@malabar.local> + + * src/timeresp.py, src/matlab.py: moved documentation about time + series convention from matlab.py to timeresp.py + + * examples/pvtol-nested-ss.py: Fixed bug in call to step (wrong + second argument) + + * tests/matlab_test.py: Updated tests to use MATLAB time response + conventions. + + * tests/timeresp_test.py: Created unit tests for timeresp module, + based on matlab_test.py + +2011-06-17 Richard Murray <murray@malabar.local> + + * src/timeresp.py (ForcedResponse): swapped order of input and time + arguments for linear response, following Eike's comment "T must + always be supplied by the user, but U has a useful default value of + 0." + + * src/matlab.py: moved code for lsim, initial, step, and impulse to + timeresp.py and put in new routes that call timeresp.* versions of + the functions with transposeData set to True. + + * src/timesim.py (_check_convert_array): added transpose argument + that will transpose input data before processing it. + + * src/timesim.py: renamed lsim, initial, step, and impulse functions + to ForcedResponse, InitialResponse, StepResponse and + ImpulseResponse. These versions use Eike Welk's input ordering. + + * examples/pvtol-nested.py: calls to step() had screwed up inputs. + Fixed. + +2011-06-17 Richard Murray <murray@malabar.local> + + * src/matlab.py: added MIMO extensions from Eike Welk on 12 Jun + 2011: adds MIMO capabilities for ``lsim``, ``step``, ``impulse``, + ``initial`` + + * src/matlab.py: added changes from Eike Welk on 12 May 2011: + + - An implementation of the four simulation functions ``lsim``, + ``step``, ``initial``, and ``impulse`` of the module ``matlab``. + + - Adds a function ``dcgain`` to the ``matlab`` module, which + computes the gain of a linear system for steady state and + constant input. + + - The patch contains a bug fix for class ``StateSpace``, which + enables it to work properly together with Scipy's ``signal`` + module. + + - The simulation functions' return values are changed (back?) to + arrays, because matrices confuse Matplotlib. + + - New times series convention: see _time-series-convention section + of matlab documentation + + - SISO simulation data are squeezed on output. To turn this off, + pass the option squeeze=False + ---- control-0.4c released ----- 2011-06-17 Richard Murray <mu...@dh...> Modified: trunk/doc/conf.py =================================================================== --- trunk/doc/conf.py 2011-06-18 00:20:49 UTC (rev 160) +++ trunk/doc/conf.py 2011-06-18 19:13:09 UTC (rev 161) @@ -26,7 +26,8 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc','numpydoc'] +extensions = ['sphinx.ext.autodoc', 'numpydoc', 'sphinx.ext.pngmath', + 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -87,7 +88,11 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +#This config value contains the locations and names of other projects that +#should be linked to in this documentation. +intersphinx_mapping = {'scipy':('http://docs.scipy.org/doc/scipy/reference/', None)} + # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for Modified: trunk/examples/pvtol-lqr.py =================================================================== --- trunk/examples/pvtol-lqr.py 2011-06-18 00:20:49 UTC (rev 160) +++ trunk/examples/pvtol-lqr.py 2011-06-18 19:13:09 UTC (rev 161) @@ -119,7 +119,7 @@ subplot(221); title("Identity weights") # plot(T, Y[:,1, 1], '-', T, Y[:,2, 2], '--'); hold(True); -plot(Tx.T, Yx[0,:].T, '-', Ty.T, Yy[0,:].T, '--'); hold(True); +plot(Tx.T, Yx.T, '-', Ty.T, Yy.T, '--'); hold(True); plot([0, 10], [1, 1], 'k-'); hold(True); axis([0, 10, -0.1, 1.4]); @@ -141,9 +141,9 @@ [T3, Y3] = step(H1cx, T=linspace(0,10,100)); subplot(222); title("Effect of input weights") -plot(T1.T, Y1[0,:].T, 'b-'); hold(True); -plot(T2.T, Y2[0,:].T, 'b-'); hold(True); -plot(T3.T, Y3[0,:].T, 'b-'); hold(True); +plot(T1.T, Y1.T, 'b-'); hold(True); +plot(T2.T, Y2.T, 'b-'); hold(True); +plot(T3.T, Y3.T, 'b-'); hold(True); plot([0 ,10], [1, 1], 'k-'); hold(True); axis([0, 10, -0.1, 1.4]); @@ -162,7 +162,7 @@ subplot(223); title("Output weighting") [T2x, Y2x] = step(H2x, T=linspace(0,10,100)); [T2y, Y2y] = step(H2y, T=linspace(0,10,100)); -plot(T2x.T, Y2x[0,:].T, T2y.T, Y2y[0,:].T) +plot(T2x.T, Y2x.T, T2y.T, Y2y.T) ylabel('position'); xlabel('time'); ylabel('position'); legend(('x', 'y'), loc='lower right'); @@ -185,7 +185,7 @@ # step(H3x, H3y, 10); [T3x, Y3x] = step(H3x, T=linspace(0,10,100)); [T3y, Y3y] = step(H3y, T=linspace(0,10,100)); -plot(T3x.T, Y3x[0,:].T, T3y.T, Y3y[0,:].T) +plot(T3x.T, Y3x.T, T3y.T, Y3y.T) title("Physically motivated weights") xlabel('time'); legend(('x', 'y'), loc='lower right'); Modified: trunk/examples/pvtol-nested-ss.py =================================================================== --- trunk/examples/pvtol-nested-ss.py 2011-06-18 00:20:49 UTC (rev 160) +++ trunk/examples/pvtol-nested-ss.py 2011-06-18 19:13:09 UTC (rev 161) @@ -144,10 +144,10 @@ # 'EdgeColor', color, 'FaceColor', color); figure(9); -(Tvec, Yvec) = step(T, None, linspace(1, 20)); +(Tvec, Yvec) = step(T, linspace(1, 20)); plot(Tvec.T, Yvec.T); hold(True); -(Tvec, Yvec) = step(Co*S, None, linspace(1, 20)); +(Tvec, Yvec) = step(Co*S, linspace(1, 20)); plot(Tvec.T, Yvec.T); #TODO: PZmap for statespace systems has not yet been implemented. Modified: trunk/examples/pvtol-nested.py =================================================================== --- trunk/examples/pvtol-nested.py 2011-06-18 00:20:49 UTC (rev 160) +++ trunk/examples/pvtol-nested.py 2011-06-18 19:13:09 UTC (rev 161) @@ -134,10 +134,10 @@ # 'EdgeColor', color, 'FaceColor', color); figure(9); -(Tvec, Yvec) = step(T, None, linspace(1, 20)); +(Tvec, Yvec) = step(T, linspace(1, 20)); plot(Tvec.T, Yvec.T); hold(True); -(Tvec, Yvec) = step(Co*S, None, linspace(1, 20)); +(Tvec, Yvec) = step(Co*S, linspace(1, 20)); plot(Tvec.T, Yvec.T); figure(10); clf(); Modified: trunk/src/__init__.py =================================================================== --- trunk/src/__init__.py 2011-06-18 00:20:49 UTC (rev 160) +++ trunk/src/__init__.py 2011-06-18 19:13:09 UTC (rev 161) @@ -66,3 +66,5 @@ from modelsimp import * from rlocus import * from mateqn import * +from timeresp import ForcedResponse, InitialResponse, StepResponse, \ + ImpulseResponse Modified: trunk/src/matlab.py =================================================================== --- trunk/src/matlab.py 2011-06-18 00:20:49 UTC (rev 160) +++ trunk/src/matlab.py 2011-06-18 19:13:09 UTC (rev 161) @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """matlab.py MATLAB emulation functions. @@ -18,6 +19,8 @@ """Copyright (c) 2009 by California Institute of Technology All rights reserved. +Copyright (c) 2011 by Eike Welk + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -58,8 +61,9 @@ # Libraries that we make use of import scipy as sp # SciPy library (used all over) import numpy as np # NumPy library -import scipy.signal as signal # Signal processing library +from scipy.signal.ltisys import _default_response_times from copy import deepcopy +import warnings # Import MATLAB-like functions that are defined in other packages from scipy.signal import zpk2ss, ss2zpk, tf2zpk, zpk2tf @@ -68,8 +72,10 @@ # Control system library import ctrlutil import freqplot +import timeresp from statesp import StateSpace, _rss_generate, _convertToStateSpace from xferfcn import TransferFunction, _convertToTransferFunction +from lti import Lti #base class of StateSpace, TransferFunction from exception import ControlArgument # Import MATLAB-like functions that can be used as-is @@ -86,188 +92,216 @@ __doc__ += """ The control.matlab module defines functions that are roughly the equivalents of those in the MATLAB Control Toolbox. Items marked by a -\* are currently implemented; those marked with a ``-`` are not planned +``*`` are currently implemented; those marked with a ``-`` are not planned for implementation. -Creating linear models. -\* tf - create transfer function (TF) models - zpk - create zero/pole/gain (ZPK) models. -\* ss - create state-space (SS) models - dss - create descriptor state-space models - delayss - create state-space models with delayed terms - frd - create frequency response data (FRD) models - lti/exp - create pure continuous-time delays (TF and ZPK only) - filt - specify digital filters -- lti/set - set/modify properties of LTI models -- setdelaymodel - specify internal delay model (state space only) - -Data extraction - lti/tfdata - extract numerators and denominators - lti/zpkdata - extract zero/pole/gain data - lti/ssdata - extract state-space matrices - lti/dssdata - descriptor version of SSDATA - frd/frdata - extract frequency response data - lti/get - access values of LTI model properties - ss/getDelayModel - access internal delay model (state space only) - -Conversions -\* tf - conversion to transfer function - zpk - conversion to zero/pole/gain -\* ss - conversion to state space - frd - conversion to frequency data - c2d - continuous to discrete conversion - d2c - discrete to continuous conversion - d2d - resample discrete-time model - upsample - upsample discrete-time LTI systems -\* ss2tf - state space to transfer function - ss2zpk - transfer function to zero-pole-gain -\* tf2ss - transfer function to state space - tf2zpk - transfer function to zero-pole-gain - zpk2ss - zero-pole-gain to state space - zpk2tf - zero-pole-gain to transfer function - -System interconnections - append - group LTI models by appending inputs and outputs -\* parallel - connect LTI models in parallel (see also overloaded +) -\* series - connect LTI models in series (see also overloaded \*) -\* feedback - connect lti models with a feedback loop - lti/lft - generalized feedback interconnection - lti/connect - arbitrary interconnection of lti models - sumblk - specify summing junction (for use with connect) - strseq - builds sequence of indexed strings (for I/O naming) - -System gain and dynamics - dcgain - steady-state (D.C.) gain - lti/bandwidth - system bandwidth - lti/norm - h2 and Hinfinity norms of LTI models -\* lti/pole - system poles -\* lti/zero - system (transmission) zeros - lti/order - model order (number of states) -\* pzmap - pole-zero map (TF only) - lti/iopzmap - input/output pole-zero map - damp - natural frequency and damping of system poles - esort - sort continuous poles by real part - dsort - sort discrete poles by magnitude - lti/stabsep - stable/unstable decomposition - lti/modsep - region-based modal decomposition - -Time-domain analysis -\* step - step response - stepinfo - step response characteristics (rise time, ...) -\* impulse - impulse response - initial - free response with initial conditions -\* lsim - response to user-defined input signal - lsiminfo - linear response characteristics - gensig - generate input signal for LSIM - covar - covariance of response to white noise - -Frequency-domain analysis -\* bode - Bode plot of the frequency response - lti/bodemag - Bode magnitude diagram only - sigma - singular value frequency plot -\* nyquist - Nyquist plot -\* nichols - Nichols plot - margin - gain and phase margins - lti/allmargin - all crossover frequencies and related gain/phase margins -\* lti/freqresp - frequency response over a frequency grid -\* lti/evalfr - evaluate frequency response at given frequency - -Model simplification - minreal - minimal realization and pole/zero cancellation - ss/sminreal - structurally minimal realization (state space) -\* lti/hsvd - hankel singular values (state contributions) -\* lti/balred - reduced-order approximations of LTI models -\* ss/modred - model order reduction - -Compensator design -\* rlocus - evans root locus -\* place - pole placement - estim - form estimator given estimator gain - reg - form regulator given state-feedback and estimator gains - -LQR/LQG design - ss/lqg - single-step LQG design -\* lqr - linear-Quadratic (LQ) state-feedback regulator - dlqr - discrete-time LQ state-feedback regulator - lqry - lq regulator with output weighting - lqrd - discrete LQ regulator for continuous plant - ss/lqi - linear-Quadratic-Integral (LQI) controller - ss/kalman - Kalman state estimator - ss/kalmd - discrete Kalman estimator for continuous plant - ss/lqgreg - build LQG regulator from LQ gain and Kalman estimator - ss/lqgtrack - build LQG servo-controller - augstate - augment output by appending states - -State-space (SS) models -\* rss - random stable continuous-time state-space models -\* drss - random stable discrete-time state-space models - ss2ss - state coordinate transformation - canon - canonical forms of state-space models -\* ctrb - controllability matrix -\* obsv - observability matrix -\* gram - controllability and observability gramians - ss/prescale - optimal scaling of state-space models. - balreal - gramian-based input/output balancing - ss/xperm - reorder states. - -Frequency response data (FRD) models - frd/chgunits - change frequency vector units - frd/fcat - merge frequency responses - frd/fselect - select frequency range or subgrid - frd/fnorm - peak gain as a function of frequency - frd/abs - entrywise magnitude of the frequency response - frd/real - real part of the frequency response - frd/imag - imaginary part of the frequency response - frd/interp - interpolate frequency response data - mag2db - convert magnitude to decibels (dB) - db2mag - convert decibels (dB) to magnitude - -Time delays - lti/hasdelay - true for models with time delays - lti/totaldelay - total delay between each input/output pair - lti/delay2z - replace delays by poles at z=0 or FRD phase shift -\* pade - pade approximation of time delays - -Model dimensions and characteristics - class - model type ('tf', 'zpk', 'ss', or 'frd') - isa - test if model is of given type - tf/size - model sizes - lti/ndims - number of dimensions - lti/isempty - true for empty models - lti/isct - true for continuous-time models - lti/isdt - true for discrete-time models - lti/isproper - true for proper models - lti/issiso - true for single-input/single-output models - lti/isstable - true for models with stable dynamics - lti/reshape - reshape array of linear models - -Overloaded arithmetic operations -\* + and - - add and subtract systems (parallel connection) -\* \* - multiply systems (series connection) - / - left divide -- sys1\sys2 means inv(sys1)\*sys2 -- \ - right divide -- sys1/sys2 means sys1\*inv(sys2) - ^ - powers of a given system - ' - pertransposition - .' - transposition of input/output map - .\* - element-by-element multiplication - [..] - concatenate models along inputs or outputs - lti/stack - stack models/arrays along some array dimension - lti/inv - inverse of an LTI system - lti/conj - complex conjugation of model coefficients - -Matrix equation solvers and linear algebra -\* lyap, dlyap - solve Lyapunov equations - lyapchol, dlyapchol - square-root Lyapunov solvers -\* care, dare - solve algebraic Riccati equations - gcare, gdare - generalized Riccati solvers - bdschur - block diagonalization of a square matrix - -Additional functions -\* gangof4 - generate the Gang of 4 sensitivity plots -\* linspace - generate a set of numbers that are linearly spaced -\* logspace - generate a set of numbers that are logarithmically spaced -\* unwrap - unwrap a phase angle to give a continuous curve - +== ========================== ================================================ +**Creating linear models.** +-------------------------------------------------------------------------------- +\* :func:`tf` create transfer function (TF) models +\ zpk create zero/pole/gain (ZPK) models. +\* :func:`ss` create state-space (SS) models +\ dss create descriptor state-space models +\ delayss create state-space models with delayed terms +\ frd create frequency response data (FRD) models +\ lti/exp create pure continuous-time delays (TF and ZPK + only) +\ filt specify digital filters +\- lti/set set/modify properties of LTI models +\- setdelaymodel specify internal delay model (state space only) +\ +**Data extraction** +-------------------------------------------------------------------------------- +\ lti/tfdata extract numerators and denominators +\ lti/zpkdata extract zero/pole/gain data +\ lti/ssdata extract state-space matrices +\ lti/dssdata descriptor version of SSDATA +\ frd/frdata extract frequency response data +\ lti/get access values of LTI model properties +\ ss/getDelayModel access internal delay model (state space only) +\ +**Conversions** +-------------------------------------------------------------------------------- +\* :func:`tf` conversion to transfer function +\ zpk conversion to zero/pole/gain +\* :func:`ss` conversion to state space +\ frd conversion to frequency data +\ c2d continuous to discrete conversion +\ d2c discrete to continuous conversion +\ d2d resample discrete-time model +\ upsample upsample discrete-time LTI systems +\* :func:`ss2tf` state space to transfer function +\ ss2zpk transfer function to zero-pole-gain +\* :func:`tf2ss` transfer function to state space +\ tf2zpk transfer function to zero-pole-gain +\ zpk2ss zero-pole-gain to state space +\ zpk2tf zero-pole-gain to transfer function +\ +**System interconnections** +-------------------------------------------------------------------------------- +\ append group LTI models by appending inputs and outputs +\* :func:`parallel` connect LTI models in parallel + (see also overloaded +) +\* :func:`series` connect LTI models in series + (see also overloaded \*) +\* :func:`feedback` connect lti models with a feedback loop +\ lti/lft generalized feedback interconnection +\ lti/connect arbitrary interconnection of lti models +\ sumblk specify summing junction (for use with connect) +\ strseq builds sequence of indexed strings + (for I/O naming) +\ +**System gain and dynamics** +-------------------------------------------------------------------------------- +\* :func:`dcgain` steady-state (D.C.) gain +\ lti/bandwidth system bandwidth +\ lti/norm h2 and Hinfinity norms of LTI models +\* :func:`pole` system poles +\* :func:`zero` system (transmission) zeros +\ lti/order model order (number of states) +\* :func:`pzmap` pole-zero map (TF only) +\ lti/iopzmap input/output pole-zero map +\ damp natural frequency and damping of system poles +\ esort sort continuous poles by real part +\ dsort sort discrete poles by magnitude +\ lti/stabsep stable/unstable decomposition +\ lti/modsep region-based modal decomposition +\ +**Time-domain analysis** +-------------------------------------------------------------------------------- +\* :func:`step` step response +\ stepinfo step response characteristics (rise time, ...) +\* :func:`impulse` impulse response +\* :func:`initial` free response with initial conditions +\* :func:`lsim` response to user-defined input signal +\ lsiminfo linear response characteristics +\ gensig generate input signal for LSIM +\ covar covariance of response to white noise +\ +**Frequency-domain analysis** +-------------------------------------------------------------------------------- +\* :func:`bode` Bode plot of the frequency response +\ lti/bodemag Bode magnitude diagram only +\ sigma singular value frequency plot +\* :func:`nyquist` Nyquist plot +\* :func:`nichols` Nichols plot +\* :func:`margin` gain and phase margins +\ lti/allmargin all crossover frequencies and related gain/phase + margins +\* :func:`freqresp` frequency response over a frequency grid +\* :func:`evalfr` evaluate frequency response at given frequency +\ +**Model simplification** +-------------------------------------------------------------------------------- +\ minreal minimal realization and pole/zero cancellation +\ ss/sminreal structurally minimal realization (state space) +\* :func:`lti/hsvd` hankel singular values (state contributions) +\* :func:`lti/balred` reduced-order approximations of LTI models +\* :func:`ss/modred` model order reduction +\ +**Compensator design** +-------------------------------------------------------------------------------- +\* :func:`rlocus` evans root locus +\* :func:`place` pole placement +\ estim form estimator given estimator gain +\ reg form regulator given state-feedback and + estimator gains +\ +**LQR/LQG design** +-------------------------------------------------------------------------------- +\ ss/lqg single-step LQG design +\* :func:`lqr` linear-Quadratic (LQ) state-feedback regulator +\ dlqr discrete-time LQ state-feedback regulator +\ lqry lq regulator with output weighting +\ lqrd discrete LQ regulator for continuous plant +\ ss/lqi linear-Quadratic-Integral (LQI) controller +\ ss/kalman Kalman state estimator +\ ss/kalmd discrete Kalman estimator for continuous plant +\ ss/lqgreg build LQG regulator from LQ gain and Kalman + estimator +\ ss/lqgtrack build LQG servo-controller +\ augstate augment output by appending states +\ +**State-space (SS) models** +-------------------------------------------------------------------------------- +\* :func:`rss` random stable continuous-time state-space models +\* :func:`drss` random stable discrete-time state-space models +\ ss2ss state coordinate transformation +\ canon canonical forms of state-space models +\* :func:`ctrb` controllability matrix +\* :func:`obsv` observability matrix +\* :func:`gram` controllability and observability gramians +\ ss/prescale optimal scaling of state-space models. +\ balreal gramian-based input/output balancing +\ ss/xperm reorder states. +\ +**Frequency response data (FRD) models** +-------------------------------------------------------------------------------- +\ frd/chgunits change frequency vector units +\ frd/fcat merge frequency responses +\ frd/fselect select frequency range or subgrid +\ frd/fnorm peak gain as a function of frequency +\ frd/abs entrywise magnitude of the frequency response +\ frd/real real part of the frequency response +\ frd/imag imaginary part of the frequency response +\ frd/interp interpolate frequency response data +\ mag2db convert magnitude to decibels (dB) +\ db2mag convert decibels (dB) to magnitude +\ +**Time delays** +-------------------------------------------------------------------------------- +\ lti/hasdelay true for models with time delays +\ lti/totaldelay total delay between each input/output pair +\ lti/delay2z replace delays by poles at z=0 or FRD phase + shift +\* :func:`pade` pade approximation of time delays +\ +**Model dimensions and characteristics** +-------------------------------------------------------------------------------- +\ class model type ('tf', 'zpk', 'ss', or 'frd') +\ isa test if model is of given type +\ tf/size model sizes +\ lti/ndims number of dimensions +\ lti/isempty true for empty models +\ lti/isct true for continuous-time models +\ lti/isdt true for discrete-time models +\ lti/isproper true for proper models +\ lti/issiso true for single-input/single-output models +\ lti/isstable true for models with stable dynamics +\ lti/reshape reshape array of linear models +\ +**Overloaded arithmetic operations** +-------------------------------------------------------------------------------- +\* \+ and - add and subtract systems (parallel connection) +\* \* multiply systems (series connection) +\ / right divide -- sys1/sys2 means sys1\*inv(sys2) +\- \\ left divide -- sys1\\sys2 means inv(sys1)\*sys2 +\ ^ powers of a given system +\ ' pertransposition +\ .' transposition of input/output map +\ .\* element-by-element multiplication +\ [..] concatenate models along inputs or outputs +\ lti/stack stack models/arrays along some array dimension +\ lti/inv inverse of an LTI system +\ lti/conj complex conjugation of model coefficients +\ +**Matrix equation solvers and linear algebra** +-------------------------------------------------------------------------------- +\* lyap, dlyap solve Lyapunov equations +\ lyapchol, dlyapchol square-root Lyapunov solvers +\* care, dare solve algebraic Riccati equations +\ gcare, gdare generalized Riccati solvers +\ bdschur block diagonalization of a square matrix +\ +**Additional functions** +-------------------------------------------------------------------------------- +\* :func:`gangof4` generate the Gang of 4 sensitivity plots +\* :func:`linspace` generate a set of numbers that are linearly + spaced +\* :func:`logspace` generate a set of numbers that are + logarithmically spaced +\* :func:`unwrap` unwrap a phase angle to give a continuous curve +== ========================== ================================================ """ def ss(*args): @@ -752,9 +786,9 @@ def ngrid(): """Nichols chart grid. - Usage - ===== - ngrid() + Examples + -------- + >>> ngrid() """ from nichols import nichols_grid nichols_grid() @@ -815,134 +849,81 @@ % len(args)) return margins[0], margins[1], margins[3], margins[4] -# -# Modifications to scipy.signal functions -# -# Redefine lsim to use lsim2 -def lsim(sys, U=None, T=None, X0=None, **keywords): - """Simulate the output of a linear system - Examples - -------- - >>> T, yout, xout = lsim(sys, u, T, X0) +def dcgain(*args): + ''' + Compute the gain of the system in steady state. - Parameters - ---------- - sys: StateSpace, or TransferFunction - LTI system to simulate - u: input array giving input at each time T - T: time steps at which the input is defined - X0: initial condition (optional, default = 0) + The function takes either 1, 2, 3, or 4 parameters: - Returns - ------- - T: time values of the output - yout: response of the system - xout: time evolution of the state vector - """ - # Convert the system to an signal.lti for simulation - #! This should send a warning for MIMO systems - ltiobjs = sys.returnScipySignalLti() - ltiobj = ltiobjs[0][0] - - return sp.signal.lsim2(ltiobj, U, T, X0, **keywords) - -#! Redefine step to use lsim2 -#! Not yet implemented -def step(*args, **keywords): - """Step response of a linear system - - Examples - -------- - >>> T, yout = step(sys, T, X0) - Parameters ---------- - sys: StateSpace, or TransferFunction - T: array - T is the time vector (optional; autocomputed if not given) - X0: array - X0 is the initial condition (optional; default = 0) + A, B, C, D: array-like + A linear system in state space form. + Z, P, k: array-like, array-like, number + A linear system in zero, pole, gain form. + num, den: array-like + A linear system in transfer function form. + sys: Lti (StateSpace or TransferFunction) + A linear system object. Returns ------- - T: array - Time values of the output - yout: array - response of the system - """ - sys = args[0] - ltiobjs = sys.returnScipySignalLti() - ltiobj = ltiobjs[0][0] + gain: matrix + The gain of each output versus each input: + :math:`y = gain \cdot u` + + Notes + ----- + This function is only useful for systems with invertible system + matrix ``A``. + + All systems are first converted to state space form. The function then + computes: + + .. math:: gain = - C \cdot A^{-1} \cdot B + D + ''' + #Convert the parameters to state space form + if len(args) == 4: + A, B, C, D = args + sys = ss(A, B, C, D) + elif len(args) == 3: + Z, P, k = args + A, B, C, D = zpk2ss(Z, P, k) + sys = ss(A, B, C, D) + elif len(args) == 2: + num, den = args + sys = tf2ss(num, den) + elif len(args) == 1: + sys, = args + sys = ss(sys) + else: + raise ValueError("Function ``dcgain`` needs either 1, 2, 3 or 4 " + "arguments.") + #gain = - C * A**-1 * B + D + gain = sys.D - sys.C * sys.A.I * sys.B + return gain - out = sp.signal.step(ltiobj, **keywords) - yout = [] - yout.append(np.mat(out[0])) - yout.append(out[1]) - yout = tuple(yout) - return yout +# Simulation routines +# Call corresponding functions in timeresp, with arguments transposed -# Redefine initial to use lsim2 -#! Not yet implemented (uses step for now) -def initial(*args, **keywords): - """Initial condition response of a linear system +def step(sys, T=None, X0=0., input=0, output=0, **keywords): + T, yout = timeresp.StepResponse(sys, T, X0, input, output, + transpose = True, **keywords) + return T, yout - Examples - -------- - >>> T, yout = initial(sys, T, X0) +def impulse(sys, T=None, X0=0., input=0, output=0, **keywords): + T, yout = timeresp.ImpulseResponse(sys, T, X0, input, output, + transpose = True, **keywords) + return T, yout - Parameters - ---------- - sys: StateSpace, or TransferFunction - T: array - T is the time vector (optional; autocomputed if not given) - X0: array - X0 is the initial condition (optional; default = 0) +def initial(sys, T=None, X0=0., input=0, output=0, **keywords): + T, yout = timeresp.InitialResponse(sys, T, X0, input, output, + transpose = True, **keywords) + return T, yout - Returns - ------- - T: array - Time values of the output - yout: array - response of the system - - """ - sys = args[0] - ltiobjs = sys.returnScipySignalLti() - ltiobj = ltiobjs[0][0] - - yout = sp.signal.initial(ltiobj, **keywords) - return np.mat(yout) - -# Redefine impulse to use initial() -#! Not yet implemented (uses impulse for now) -def impulse(*args, **keywords): - """Impulse response of a linear system - - Examples - -------- - >>> T, yout = impulse(sys, T, X0) - - Parameters - ---------- - sys: StateSpace, or TransferFunction - T: array - T is the time vector (optional; autocomputed if not given) - X0: array - X0 is the initial condition (optional; default = 0) - - Returns - ------- - T: array - Time values of the output - yout: array - response of the system - - """ - sys = args[0] - ltiobjs = sys.returnScipySignalLti() - ltiobj = ltiobjs[0][0] - - yout = sp.signal.impulse(ltiobj, **keywords) - return np.mat(yout) +def lsim(sys, U=0., T=None, X0=0., **keywords): + T, yout, xout = timeresp.ForcedResponse(sys, T, U, X0, + transpose = True, **keywords) + return T, yout, xout Modified: trunk/src/statesp.py =================================================================== --- trunk/src/statesp.py 2011-06-18 00:20:49 UTC (rev 160) +++ trunk/src/statesp.py 2011-06-18 19:13:09 UTC (rev 161) @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """stateSpace.py State space representation and functions. @@ -72,8 +73,9 @@ """ -from numpy import all, angle, any, array, concatenate, cos, delete, dot, \ - empty, exp, eye, matrix, ones, pi, poly, poly1d, roots, shape, sin, zeros +from numpy import all, angle, any, array, asarray, concatenate, cos, delete, \ + dot, empty, exp, eye, matrix, ones, pi, poly, poly1d, roots, shape, sin, \ + zeros from numpy.random import rand, randn from numpy.linalg import inv, det, solve from numpy.linalg.linalg import LinAlgError @@ -427,8 +429,8 @@ for i in range(self.outputs): for j in range(self.inputs): - out[i][j] = lti(self.A, self.B[:, j], self.C[i, :], - self.D[i, j]) + out[i][j] = lti(asarray(self.A), asarray(self.B[:, j]), + asarray(self.C[i, :]), asarray(self.D[i, j])) return out Added: trunk/src/timeresp.py =================================================================== --- trunk/src/timeresp.py (rev 0) +++ trunk/src/timeresp.py 2011-06-18 19:13:09 UTC (rev 161) @@ -0,0 +1,652 @@ +# timesim.py - time-domain simulation routes +"""timesim.py + +Time domain simulation. + +Copyright (c) 2010 by SciPy Developers + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the California Institute of Technology nor + the names of its contributors may be used to endorse or promote + products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +Author: Eike Welk +Date: 12 May 2011 +$Id: matlab.py 157 2011-06-17 23:51:46Z murrayrm $ + +.. _time-series-convention: + +Convention for Time Series +-------------------------- + +This is a convention for function arguments and return values that +represent time series: sequences of values that change over time. It is used +throughout the library, for example in the functions :func:`lsim`, :func:`step`, +:func:`impulse`, and :func:`initial`. + +.. note:: + This convention is different from the convention used in the library + :mod:`scipy.signal`. In Scipy's convention the meaning of rows and columns + is interchanged. All 2D values must be transposed when they are used with + functions from :mod:`scipy.signal`. + +Types: + + * **Arguments** can be **arrays**, **matrices**, or **nested lists**. + * **Return values** are **arrays** (not matrices). + +The time vector is either 1D, or 2D with shape (1, n):: + + T = [[t1, t2, t3, ..., tn ]] + +Input, state, and output all follow the same convention. Columns are different +points in time, rows are different components. When there is only one row, a +1D object is accepted or returned, which adds convenience for SISO systems:: + + U = [[u1(t1), u1(t2), u1(t3), ..., u1(tn)] + [u2(t1), u2(t2), u2(t3), ..., u2(tn)] + ... + ... + [ui(t1), ui(t2), ui(t3), ..., ui(tn)]] + + Same for X, Y + +The initial conditions are either 1D, or 2D with shape (j, 1):: + + X0 = [[x1] + [x2] + ... + ... + [xj]] + +So, U[:,2] is the system's input at the third point in time; and U[1] or U[1,:] +is the sequence of values for the system's second input. + +As all simulation functions return *arrays*, plotting is convenient:: + + t, y = step(sys) + plot(t, y) + +The output of a MIMO system can be plotted like this:: + + t, y, x = lsim(sys, u, t) + plot(t, y[0], label='y_0') + plot(t, y[1], label='y_1') + +The convention also works well with the state space form of linear systems. If +``D`` is the feedthrough *matrix* of a linear system, and ``U`` is its input +(*matrix* or *array*), then the feedthrough part of the system's response, +can be computed like this:: + + ft = D * U + +---------------------------------------------------------------- + +""" + +# Libraries that we make use of +import scipy as sp # SciPy library (used all over) +import numpy as np # NumPy library +from scipy.signal.ltisys import _default_response_times +from copy import deepcopy +import warnings +from statesp import StateSpace, _rss_generate, _convertToStateSpace +from lti import Lti # base class of StateSpace, TransferFunction + +# +# Solve the systems's differential equations +# + +def _check_convert_array(in_obj, legal_shapes, err_msg_start, squeeze=False, + transpose=False): + """ + Helper function for checking array-like parameters. + + * Check type and shape of ``in_obj``. + * Convert ``in_obj`` to an array if necessary. + * Change shape of ``in_obj`` according to parameter ``squeeze``. + * If ``in_obj`` is a scalar (number) it is converted to an array with + a legal shape, that is filled with the scalar value. + + The function raises an exception when it detects an error. + + Parameters: + + in_obj: array like object + The array or matrix which is checked. + + legal_shapes: list of tuple + A list of shapes that in_obj can legally have. + The special value "any" means that there can be any + number of elements in a certain dimension. + + * ``(2, 3)`` describes an array with 2 rows and 3 columns + * ``(2, "any")`` describes an array with 2 rows and any number of + columns + + err_msg_start: str + String that is prepended to the error messages, when this function + raises an exception. It should be used to identify the argument which + is currently checked. + + squeeze: bool + If True, all dimensions with only one element are removed from the + array. If False the array's shape is unmodified. + + For example: + ``array([[1,2,3]])`` is converted to ``array([1, 2, 3])`` + + transpose: bool + If True, assume that input arrays are transposed for the standard + format. Used to convert MATLAB-style inputs to our format. + + Returns: + + out_array: array + The checked and converted contents of ``in_obj``. + """ + #convert nearly everything to an array. + out_array = np.asarray(in_obj) + if (transpose): + out_array = np.transpose(out_array) + + #Test element data type, elements must be numbers + legal_kinds = set(("i", "f", "c")) #integer, float, complex + if out_array.dtype.kind not in legal_kinds: + err_msg = "Wrong element data type: '{d}'. Array elements " \ + "must be numbers.".format(d=str(out_array.dtype)) + raise TypeError(err_msg_start + err_msg) + + #If array is zero dimensional (in_obj is scalar): + #create array with legal shape filled with the original value. + if out_array.ndim == 0: + for s_legal in legal_shapes: + #search for shape that does not contain the special symbol any. + if "any" in s_legal: + continue + the_val = out_array[()] + out_array = np.empty(s_legal, 'd') + out_array.fill(the_val) + break + + #Test shape + def shape_matches(s_legal, s_actual): + """Test if two shape tuples match""" + #Array must have required number of dimensions + if len(s_legal) != len(s_actual): + return False + #All dimensions must contain required number of elements. Joker: "all" + for n_legal, n_actual in zip(s_legal, s_actual): + if n_legal == "any": + continue + if n_legal != n_actual: + return False + return True + + #Iterate over legal shapes, and see if any matches out_array's shape. + for s_legal in legal_shapes: + if shape_matches(s_legal, out_array.shape): + break + else: + legal_shape_str = " or ".join([str(s) for s in legal_shapes]) + err_msg = "Wrong shape (rows, columns): {a}. Expected: {e}." \ + .format(e=legal_shape_str, a=str(out_array.shape)) + raise ValueError(err_msg_start + err_msg) + + #Convert shape + if squeeze: + out_array = np.squeeze(out_array) + #We don't want zero dimensional arrays + if out_array.shape == tuple(): + out_array = out_array.reshape((1,)) + + return out_array + +# Forced response of a linear system +def ForcedResponse(sys, T=None, U=0., X0=0., transpose=False, **keywords): + """Simulate the output of a linear system. + + As a convenience for parameters `U`, `X0`: + Numbers (scalars) are converted to constant arrays with the correct shape. + The correct shape is inferred from arguments `sys` and `T`. + + For information on the **shape** of parameters `U`, `T`, `X0` and + return values `T`, `yout`, `xout` see: :ref:`time-series-convention` + + Parameters + ---------- + sys: Lti (StateSpace, or TransferFunction) + LTI system to simulate + + T: array-like + Time steps at which the input is defined, numbers must be (strictly + monotonic) increasing. + + U: array-like or number, optional + Input array giving input at each time `T` (default = 0). + + If `U` is ``None`` or ``0``, a special algorithm is used. This special + algorithm is faster than the general algorithm, which is used otherwise. + + X0: array-like or number, optional + Initial condition (default = 0). + + transpose: bool + If True, transpose all input and output arrays (for backward + compatibility with MATLAB and scipy.signal.lsim) + + **keywords: + Additional keyword arguments control the solution algorithm for the + differential equations. These arguments are passed on to the function + :func:`scipy.integrate.odeint`. See the documentation for + :func:`scipy.integrate.odeint` for information about these + arguments. + + Returns + ------- + T: array + Time values of the output. + yout: array + Response of the system. + xout: array + Time evolution of the state vector. + + See Also + -------- + StepResponse, InitialResponse, ImpulseResponse + + Examples + -------- + >>> T, yout, xout = ForcedResponse(sys, T, u, X0) + """ + if not isinstance(sys, Lti): + raise TypeError('Parameter ``sys``: must be a ``Lti`` object. ' + '(For example ``StateSpace`` or ``TransferFunction``)') + sys = _convertToStateSpace(sys) + A, B, C, D = np.asarray(sys.A), np.asarray(sys.B), np.asarray(sys.C), \ + np.asarray(sys.D) +# d_type = A.dtype + n_states = A.shape[0] + n_inputs = B.shape[1] + + #Test if T has shape (n,) or (1, n); + #T must be array-like and values must be increasing. + #The length of T determines the length of the input vector. + if T is None: + raise ValueError('Parameter ``T``: must be array-like, and contain ' + '(strictly monotonic) increasing numbers.') + T = _check_convert_array(T, [('any',), (1,'any')], + 'Parameter ``T``: ', squeeze=True, + transpose = transpose) + if not all(T[1:] - T[:-1] > 0): + raise ValueError('Parameter ``T``: time values must be ' + '(strictly monotonic) increasing numbers.') + n_steps = len(T) #number of simulation steps + + #create X0 if not given, test if X0 has correct shape + X0 = _check_convert_array(X0, [(n_states,), (n_states,1)], + 'Parameter ``X0``: ', squeeze=True) + + #Solve the differential equation, copied from scipy.signal.ltisys. + dot, squeeze, = np.dot, np.squeeze #Faster and shorter code + #Faster algorithm if U is zero + if U is None or (isinstance(U, (int, float)) and U == 0): + #Function that computes the time derivative of the linear system + def f_dot(x, _t): + return dot(A,x) + + xout = sp.integrate.odeint(f_dot, X0, T, **keywords) + yout = dot(C, xout.T) + #General algorithm that interpolates U in between output points + else: + #Test if U has correct shape and type + legal_shapes = [(n_steps,), (1,n_steps)] if n_inputs == 1 else \ + [(n_inputs, n_steps)] + U = _check_convert_array(U, legal_shapes, + 'Parameter ``U``: ', squeeze=False, + transpose=transpose) + #convert 1D array to D2 array with only one row + if len(U.shape) == 1: + U = U.reshape(1,-1) #pylint: disable=E1103 + + #Create a callable that uses linear interpolation to + #calculate the input at any time. + compute_u = sp.interpolate.interp1d(T, U, kind='linear', copy=False, + axis=-1, bounds_error=False, + fill_value=0) + + #Function that computes the time derivative of the linear system + def f_dot(x, t): + return dot(A,x) + squeeze(dot(B,compute_u([t]))) + + xout = sp.integrate.odeint(f_dot, X0, T, **keywords) + yout = dot(C, xout.T) + dot(D, U) + + yout = squeeze(yout) + xout = xout.T + + # See if we need to transpose the data back into MATLAB form + if (transpose): + T = np.transpose(T) + yout = np.transpose(yout) + xout = np.transpose(xout) + + return T, yout, xout + +def StepResponse(sys, T=None, X0=0., input=0, output=0, \ + transpose = False, **keywords): + #pylint: disable=W0622 + """Step response of a linear system + + If the system has multiple inputs or outputs (MIMO), one input and one + output have to be selected for the simulation. The parameters `input` + and `output` do this. All other inputs are set to 0, all other outputs + are ignored. + + For information on the **shape** of parameters `T`, `X0` and + return values `T`, `yout` see: :ref:`time-series-convention` + + Parameters + ---------- + sys: StateSpace, or TransferFunction + LTI system to simulate + + T: array-like object, optional + Time vector (argument is autocomputed if not given) + + X0: array-like or number, optional + Initial condition (default = 0) + + Numbers are converted to constant arrays with the correct shape. + + input: int + Index of the input that will be used in this simulation. + + output: int + Index of the output that will be used in this simulation. + + transpose: bool + If True, transpose all input and output arrays (for backward + compatibility with MATLAB and scipy.signal.lsim) + + **keywords: + Additional keyword arguments control the solution algorithm for the + differential equations. These arguments are passed on to the function + :func:`lsim`, which in turn passes them on to + :func:`scipy.integrate.odeint`. See the documentation for + :func:`scipy.integrate.odeint` for information about these + arguments. + + Returns + ------- + T: array + Time values of the output + + yout: array + Response of the system + + See Also + -------- + lsim, initial, impulse + + Examples + -------- + >>> T, yout = step(sys, T, X0) + """ + sys = _convertToStateSpace(sys) + sys = _mimo2siso(sys, input, output, warn_conversion=True) + if T is None: + T = _default_response_times(sys.A, 100) + U = np.ones_like(T) + + T, yout, _xout = ForcedResponse(sys, T, U, X0, + transpose = transpose, **keywords) + + return T, yout + + +def InitialResponse(sys, T=None, X0=0., input=0, output=0, transpose=False, + **keywords): + #pylint: disable=W0622 + """Initial condition response of a linear system + + If the system has multiple inputs or outputs (MIMO), one input and one + output have to be selected for the simulation. The parameters `input` + and `output` do this. All other inputs are set to 0, all other outputs + are ignored. + + For information on the **shape** of parameters `T`, `X0` and + return values `T`, `yout` see: :ref:`time-series-convention` + + Parameters + ---------- + sys: StateSpace, or TransferFunction + LTI system to simulate + + T: array-like object, optional + Time vector (argument is autocomputed if not given) + + X0: array-like object or number, optional + Initial condition (default = 0) + + Numbers are converted to constant arrays with the correct shape. + + input: int + Index of the input that will be used in this simulation. + + output: int + Index of the output that will be used in this simulation. + + transpose: bool + If True, transpose all input and output arrays (for backward + compatibility with MATLAB and scipy.signal.lsim) + + **keywords: + Additional keyword arguments control the solution algorithm for the + differential equations. These arguments are passed on to the function + :func:`lsim`, which in turn passes them on to + :func:`scipy.integrate.odeint`. See the documentation for + :func:`scipy.integrate.odeint` for information about these + arguments. + + + Returns + ------- + T: array + Time values of the output + yout: array + Response of the system + + See Also + -------- + lsim, step, impulse + + Examples + -------- + >>> T, yout = InitialResponsesys, T, X0) + """ + sys = _convertToStateSpace(sys) + sys = _mimo2siso(sys, input, output, warn_conversion=True) + #Create time and input vectors; checking is done in ForcedResponse(...) + #The initial vector X0 is created in ForcedResponse(...) if necessary + if T is None: + T = _default_response_times(sys.A, 100) + U = np.zeros_like(T) + + T, yout, _xout = ForcedResponse(sys, T, U, X0, **keywords) + return T, yout + + +def ImpulseResponse(sys, T=None, X0=0., input=0, output=0, + transpose=False, **keywords): + #pylint: disable=W0622 + """Impulse response of a linear system + + If the system has multiple inputs or outputs (MIMO), one input and one + output have to be selected for the simulation. The parameters `input` + and `output` do this. All other inputs are set to 0, all other outputs + are ignored. + + For information on the **shape** of parameters `T`, `X0` and + return values `T`, `yout` see: :ref:`time-series-convention` + + Parameters + ---------- + sys: StateSpace, TransferFunction + LTI system to simulate + + T: array-like object, optional + Time vector (argument is autocomputed if not given) + + X0: array-like object or number, optional + Initial condition (default = 0) + + Numbers are converted to constant arrays with the correct shape. + + input: int + Index of the input that will be used in this simulation. + + output: int + Index of the output that will be used in this simulation. + + transpose: bool + If True, transpose all input and output arrays (for backward + compatibility with MATLAB and scipy.signal.lsim) + + **keywords: + Additional keyword arguments control the solution algorithm for the + differential equations. These arguments are passed on to the function + :func:`lsim`, which in turn passes them on to + :func:`scipy.integrate.odeint`. See the documentation for + :func:`scipy.integrate.odeint` for information about these + arguments. + + + Returns + ------- + T: array + Time values of the output + yout: array + Response of the system + + See Also + -------- + lsim, step, initial + + Examples + -------- + >>> T, yout = ImpulseResponse(sys, T, X0) + """ + sys = _convertToStateSpace(sys) + sys = _mimo2siso(sys, input, output, warn_conversion=True) + + #System has direct feedthrough, can't simulate impulse response numerically. + if np.any(sys.D != 0): + warnings.warn('System has direct feedthrough: ``D != 0``. The infinite ' + 'impulse at ``t=0`` does not appear in the output. \n' + 'Results may be meaningless!') + + #create X0 if not given, test if X0 has correct shape. + #Must be done here because it is used for computations here. + n_states = sys.A.shape[0] + X0 = _check_convert_array(X0, [(n_states,), (n_states,1)], + 'Parameter ``X0``: \n', squeeze=True) + + #Compute new X0 that contains the impulse + #We can't put the impulse into U because there is no numerical + #representation for it (infinitesimally short, infinitely high). + #See also: http://www.mathworks.com/support/tech-notes/1900/1901.html + B = np.asarray(sys.B).squeeze() + new_X0 = B + X0 + + #Compute T and U, no checks necessary, they will be checked in lsim + if T is None: + T = _default_response_times(sys.A, 100) + U = np.zeros_like(T) + + T, yout, _xout = ForcedResponse(sys, T, U, new_X0, \ + transpose=transpose, **keywords) + return T, yout + +#! TODO: this function probably belongs in a different file +def _mimo2siso(sys, input, output, warn_conversion): + #pylint: disable=W0622 + """ + Convert a MIMO system to a SISO system. (Convert a system with multiple + inputs and/or outputs, to a system with a single input and output.) + + The input and output that are used in the SISO system can be selected + with the parameters ``input`` and ``output``. All other inputs are set + to 0, all other outputs are ignored. + + If ``sys`` is already a SISO system, it will be returned unaltered. + + Parameters: + + sys... [truncated message content] |
From: <mur...@us...> - 2011-06-22 06:02:10
|
Revision: 162 http://python-control.svn.sourceforge.net/python-control/?rev=162&view=rev Author: murrayrm Date: 2011-06-22 06:02:02 +0000 (Wed, 22 Jun 2011) Log Message: ----------- Added Eike Welk's documenation changes + a few small tweaks (see ChangeLog) Modified Paths: -------------- trunk/ChangeLog trunk/doc/conf.py trunk/doc/index.rst trunk/doc/matlab_strings.rst trunk/src/ctrlutil.py trunk/src/delay.py trunk/src/freqplot.py trunk/src/mateqn.py trunk/src/matlab.py trunk/src/modelsimp.py trunk/src/nichols.py trunk/src/pzmap.py trunk/src/statefbk.py trunk/src/statesp.py trunk/src/timeresp.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/ChangeLog 2011-06-22 06:02:02 UTC (rev 162) @@ -1,3 +1,21 @@ +2011-06-21 Richard Murray <murray@malabar.local> + + * src/statesp.py (_mimo2siso): Moved function from matlab.py. + + * src/timeresp.py: added file documentation + split out and updated + copyright info. Small corrections to documentation. + (InitialResponse): Added missing transpose argument in call to + ForcedResponse + + * src/matlab.py: minor changes to documentation to avoid line wraps + on standard (80 col) terminal window + + * src/matlab.py: removed time-series convention documentation from + matlab.py since current MATLAB version uses standard conventions. + This documentation is currently in timeresp.py. + + * src/*, doc/*: added Eike Welk's documentation modifications + 2011-06-18 Richard Murray <murray@malabar.local> * src/timeresp.py, src/matlab.py: moved documentation about time Modified: trunk/doc/conf.py =================================================================== --- trunk/doc/conf.py 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/doc/conf.py 2011-06-22 06:02:02 UTC (rev 162) @@ -19,6 +19,7 @@ sys.path.insert(0, os.path.abspath('.')) sys.path.append(os.path.abspath('../src')) + # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. @@ -26,8 +27,14 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# Additional interesting extensions: +# ``sphinx.ext.autosummary`` : Generate function/method/attribute summary +# lists +# ``sphinx.ext.extlinks`` : Shorten external links +# ``sphinx.ext.viewcode`` : Include highlighted source code in the +# documentation extensions = ['sphinx.ext.autodoc', 'numpydoc', 'sphinx.ext.pngmath', - 'sphinx.ext.intersphinx'] + 'sphinx.ext.intersphinx', 'sphinx.ext.todo'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -90,9 +97,15 @@ #This config value contains the locations and names of other projects that #should be linked to in this documentation. -intersphinx_mapping = {'scipy':('http://docs.scipy.org/doc/scipy/reference/', None)} +intersphinx_mapping = \ + {'scipy':('http://docs.scipy.org/doc/scipy/reference/', None), + 'numpy':('http://docs.scipy.org/doc/numpy/reference/', None)} +#If this is True, todo and todolist produce output, else they produce nothing. +#The default is False. +todo_include_todos = True + # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for Modified: trunk/doc/index.rst =================================================================== --- trunk/doc/index.rst 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/doc/index.rst 2011-06-22 06:02:02 UTC (rev 162) @@ -23,3 +23,6 @@ * :ref:`modindex` * :ref:`search` +---------------------------------- + +.. todolist:: Modified: trunk/doc/matlab_strings.rst =================================================================== --- trunk/doc/matlab_strings.rst 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/doc/matlab_strings.rst 2011-06-22 06:02:02 UTC (rev 162) @@ -4,4 +4,24 @@ The Matlab Module ================= .. automodule:: matlab - :members: \ No newline at end of file + :members: + +.. todo:: + The following functions should be documented in their own modules! + This is only a temporary solution. + +.. autofunction:: pzmap.pzmap +.. autofunction:: freqplot.nyquist +.. autofunction:: nichols.nichols +.. autofunction:: statefbk.place +.. autofunction:: statefbk.lqr +.. autofunction:: statefbk.ctrb +.. autofunction:: statefbk.obsv +.. autofunction:: statefbk.gram +.. autofunction:: delay.pade +.. autofunction:: freqplot.gangof4 +.. autofunction:: ctrlutil.unwrap +.. autofunction:: mateqn.lyap +.. autofunction:: mateqn.dlyap +.. autofunction:: mateqn.care +.. autofunction:: mateqn.dare Modified: trunk/src/ctrlutil.py =================================================================== --- trunk/src/ctrlutil.py 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/src/ctrlutil.py 2011-06-22 06:02:02 UTC (rev 162) @@ -52,8 +52,6 @@ def unwrap(angle, period=2*pi): """Unwrap a phase angle to give a continuous curve - - Usage: Y = unwrap(X, period=2``*``pi) Parameters ---------- @@ -66,6 +64,14 @@ ------- Y : array_like Output array, with jumps of period/2 eliminated + + Examples + -------- + >>> import numpy as np + >>> X = [5.74, 5.97, 6.19, 0.13, 0.35, 0.57] + >>> unwrap(X, period=2 * np.pi) + [5.74, 5.97, 6.19, 6.413185307179586, 6.633185307179586, 6.8531853071795865] + """ wrap = 0; last = angle[0]; Modified: trunk/src/delay.py =================================================================== --- trunk/src/delay.py 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/src/delay.py 2011-06-22 06:02:02 UTC (rev 162) @@ -40,17 +40,29 @@ # # $Id$ +from __future__ import division + + def pade(T, n=1): """ + Create a linear system that approximates a delay. + Return the numerator and denominator coefficients of the Pade approximation. - Inputs: - T --> time delay - n --> order of approximation + Parameters + ---------- + T : number + time delay + n : integer + order of approximation - Outputs: - num, den --> arrays in descending powers of s. - + Returns + ------- + num, den : array + Polynomial coefficients of the delay model, in descending powers of s. + + Notes + ----- Based on an algorithm in Golub and van Loan, "Matrix Computation" 3rd. Ed. pp. 572-574. """ Modified: trunk/src/freqplot.py =================================================================== --- trunk/src/freqplot.py 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/src/freqplot.py 2011-06-22 06:02:02 UTC (rev 162) @@ -59,10 +59,6 @@ color=None, Plot=True): """Bode plot for a system - Usage - ===== - (mag, phase, omega) = bode(syslist, omega=None, dB=False, Hz=False, color=None, deg=True, Plot=True) - Plots a Bode plot for the system over a (optional) frequency range. Parameters @@ -82,17 +78,26 @@ Plot : boolean If True, plot magnitude and phase - Return values - ------------- - mag : magnitude array (list if len(syslist) > 1) - phase : phase array (list if len(syslist) > 1) - omega : frequency array (list if len(syslist) > 1) - + Returns + ------- + mag : array (list if len(syslist) > 1) + magnitude + phase : array (list if len(syslist) > 1) + phase + omega : array (list if len(syslist) > 1) + frequency + Notes ----- 1. Alternatively, you may use the lower-level method (mag, phase, freq) = sys.freqresp(freq) to generate the frequency response for a system, but it returns a MIMO response. + + Examples + -------- + >>> from matlab import ss + >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") + >>> mag, phase, omega = bode(sys) """ # If argument was a singleton, turn it into a list if (not getattr(syslist, '__iter__', False)): @@ -176,26 +181,31 @@ def nyquist(syslist, omega=None, Plot=True): """Nyquist plot for a system - Usage - ===== - real, imag, freq = nyquist(sys, omega=None, Plot=True) - Plots a Nyquist plot for the system over a (optional) frequency range. Parameters ---------- - syslist : linsys + syslist : list of Lti List of linear input/output systems (single system is OK) omega : freq_range Range of frequencies (list or bounds) in rad/sec Plot : boolean if True, plot magnitude - Return values - ------------- - real : real part of the frequency response array - imag : imaginary part of the frequency response array - freq : frequencies + Returns + ------- + real : array + real part of the frequency response array + imag : array + imaginary part of the frequency response array + freq : array + frequencies + + Examples + -------- + >>> from matlab import ss + >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") + >>> real, imag, freq = nyquist(sys) """ # If argument was a singleton, turn it into a list if (not getattr(syslist, '__iter__', False)): @@ -238,22 +248,18 @@ def gangof4(P, C, omega=None): """Plot the "Gang of 4" transfer functions for a system - Usage - ===== - gangof4(P, C, omega=None) - Generates a 2x2 plot showing the "Gang of 4" sensitivity functions [T, PS; CS, S] Parameters ---------- - P, C : linsys + P, C : Lti Linear input/output systems (process and control) - omega : freq_range + omega : array Range of frequencies (list or bounds) in rad/sec - Return values - ------------- + Returns + ------- None """ if (P.inputs > 1 or P.outputs > 1 or C.inputs > 1 or C.outputs >1): @@ -400,22 +406,24 @@ """Compute a reasonable default frequency range for frequency domain plots. - Usage - ===== - omega = default_frequency_range(syslist) - Finds a reasonable default frequency range by examining the features (poles and zeros) of the systems in syslist. Parameters ---------- - syslist : linsys + syslist : list of Lti List of linear input/output systems (single system is OK) - Return values - ------------- - omega : freq_range + Return + ------ + omega : array Range of frequencies in rad/sec + + Examples + -------- + >>> from matlab import ss + >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") + >>> omega = default_frequency_range(sys) """ # This code looks at the poles and zeros of all of the systems that # we are plotting and sets the frequency range to be one decade above Modified: trunk/src/mateqn.py =================================================================== --- trunk/src/mateqn.py 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/src/mateqn.py 2011-06-22 06:02:02 UTC (rev 162) @@ -38,9 +38,9 @@ """ from numpy.linalg import inv -from scipy import shape,size,asarray,copy,zeros,eye,dot +from scipy import shape, size, asarray, copy, zeros, eye, dot -from control.exception import ControlSlycot,ControlArgument +from exception import ControlSlycot, ControlArgument #### Lyapunov equation solvers lyap and dlyap Modified: trunk/src/matlab.py =================================================================== --- trunk/src/matlab.py 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/src/matlab.py 2011-06-22 06:02:02 UTC (rev 162) @@ -21,6 +21,7 @@ Copyright (c) 2011 by Eike Welk + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -61,13 +62,11 @@ # Libraries that we make use of import scipy as sp # SciPy library (used all over) import numpy as np # NumPy library -from scipy.signal.ltisys import _default_response_times from copy import deepcopy -import warnings # Import MATLAB-like functions that are defined in other packages from scipy.signal import zpk2ss, ss2zpk, tf2zpk, zpk2tf -from scipy import linspace, logspace +from numpy import linspace, logspace # Control system library import ctrlutil @@ -89,39 +88,56 @@ from modelsimp import hsvd, balred, modred from mateqn import lyap, dlyap, dare, care -__doc__ += """ -The control.matlab module defines functions that are roughly the -equivalents of those in the MATLAB Control Toolbox. Items marked by a -``*`` are currently implemented; those marked with a ``-`` are not planned -for implementation. +__doc__ += r""" +The following tables give an overview of the module ``control.matlab``. +They also show the implementation progress and the planned features of the +module. -== ========================== ================================================ -**Creating linear models.** --------------------------------------------------------------------------------- +The symbols in the first column show the current state of a feature: + +* ``*`` : The feature is currently implemented. +* ``-`` : The feature is not planned for implementation. +* ``s`` : A similar feature from an other library (Scipy) is imported into + the module, until the feature is implemented here. + + +Creating linear models +---------------------------------------------------------------------------- + +== ========================== ============================================ \* :func:`tf` create transfer function (TF) models \ zpk create zero/pole/gain (ZPK) models. \* :func:`ss` create state-space (SS) models \ dss create descriptor state-space models \ delayss create state-space models with delayed terms \ frd create frequency response data (FRD) models -\ lti/exp create pure continuous-time delays (TF and ZPK - only) +\ lti/exp create pure continuous-time delays (TF and + ZPK only) \ filt specify digital filters \- lti/set set/modify properties of LTI models -\- setdelaymodel specify internal delay model (state space only) -\ -**Data extraction** --------------------------------------------------------------------------------- +\- setdelaymodel specify internal delay model (state space + only) +== ========================== ============================================ + + +Data extraction +---------------------------------------------------------------------------- + +== ========================== ============================================ \ lti/tfdata extract numerators and denominators \ lti/zpkdata extract zero/pole/gain data \ lti/ssdata extract state-space matrices \ lti/dssdata descriptor version of SSDATA \ frd/frdata extract frequency response data \ lti/get access values of LTI model properties -\ ss/getDelayModel access internal delay model (state space only) -\ -**Conversions** --------------------------------------------------------------------------------- +\ ss/getDelayModel access internal delay model (state space) +== ========================== ============================================ + + +Conversions +---------------------------------------------------------------------------- + +== ========================== ============================================ \* :func:`tf` conversion to transfer function \ zpk conversion to zero/pole/gain \* :func:`ss` conversion to state space @@ -131,133 +147,176 @@ \ d2d resample discrete-time model \ upsample upsample discrete-time LTI systems \* :func:`ss2tf` state space to transfer function -\ ss2zpk transfer function to zero-pole-gain +\s ss2zpk transfer function to zero-pole-gain \* :func:`tf2ss` transfer function to state space -\ tf2zpk transfer function to zero-pole-gain -\ zpk2ss zero-pole-gain to state space -\ zpk2tf zero-pole-gain to transfer function -\ -**System interconnections** --------------------------------------------------------------------------------- -\ append group LTI models by appending inputs and outputs -\* :func:`parallel` connect LTI models in parallel - (see also overloaded +) -\* :func:`series` connect LTI models in series - (see also overloaded \*) -\* :func:`feedback` connect lti models with a feedback loop +\s tf2zpk transfer function to zero-pole-gain +\s zpk2ss zero-pole-gain to state space +\s zpk2tf zero-pole-gain to transfer function +== ========================== ============================================ + + +System interconnections +---------------------------------------------------------------------------- + +== ========================== ============================================ +\ append group LTI models by appending inputs/outputs +\* :func:`~bdalg.parallel` connect LTI models in parallel + (see also overloaded ``+``) +\* :func:`~bdalg.series` connect LTI models in series + (see also overloaded ``*``) +\* :func:`~bdalg.feedback` connect lti models with a feedback loop \ lti/lft generalized feedback interconnection \ lti/connect arbitrary interconnection of lti models -\ sumblk specify summing junction (for use with connect) +\ sumblk summing junction (for use with connect) \ strseq builds sequence of indexed strings (for I/O naming) -\ -**System gain and dynamics** --------------------------------------------------------------------------------- +== ========================== ============================================ + + +System gain and dynamics +---------------------------------------------------------------------------- + +== ========================== ============================================ \* :func:`dcgain` steady-state (D.C.) gain \ lti/bandwidth system bandwidth \ lti/norm h2 and Hinfinity norms of LTI models \* :func:`pole` system poles \* :func:`zero` system (transmission) zeros \ lti/order model order (number of states) -\* :func:`pzmap` pole-zero map (TF only) +\* :func:`~pzmap.pzmap` pole-zero map (TF only) \ lti/iopzmap input/output pole-zero map -\ damp natural frequency and damping of system poles +\ damp natural frequency, damping of system poles \ esort sort continuous poles by real part \ dsort sort discrete poles by magnitude \ lti/stabsep stable/unstable decomposition \ lti/modsep region-based modal decomposition -\ -**Time-domain analysis** --------------------------------------------------------------------------------- +== ========================== ============================================ + + +Time-domain analysis +---------------------------------------------------------------------------- + +== ========================== ============================================ \* :func:`step` step response -\ stepinfo step response characteristics (rise time, ...) +\ stepinfo step response characteristics \* :func:`impulse` impulse response \* :func:`initial` free response with initial conditions \* :func:`lsim` response to user-defined input signal \ lsiminfo linear response characteristics \ gensig generate input signal for LSIM \ covar covariance of response to white noise -\ -**Frequency-domain analysis** --------------------------------------------------------------------------------- +== ========================== ============================================ + + +Frequency-domain analysis +---------------------------------------------------------------------------- + +== ========================== ============================================ \* :func:`bode` Bode plot of the frequency response \ lti/bodemag Bode magnitude diagram only \ sigma singular value frequency plot -\* :func:`nyquist` Nyquist plot -\* :func:`nichols` Nichols plot +\* :func:`~freqplot.nyquist` Nyquist plot +\* :func:`~nichols.nichols` Nichols plot \* :func:`margin` gain and phase margins -\ lti/allmargin all crossover frequencies and related gain/phase - margins +\ lti/allmargin all crossover frequencies and margins \* :func:`freqresp` frequency response over a frequency grid -\* :func:`evalfr` evaluate frequency response at given frequency -\ -**Model simplification** --------------------------------------------------------------------------------- -\ minreal minimal realization and pole/zero cancellation -\ ss/sminreal structurally minimal realization (state space) -\* :func:`lti/hsvd` hankel singular values (state contributions) -\* :func:`lti/balred` reduced-order approximations of LTI models -\* :func:`ss/modred` model order reduction -\ -**Compensator design** --------------------------------------------------------------------------------- +\* :func:`evalfr` frequency response at single frequency +== ========================== ============================================ + + +Model simplification +---------------------------------------------------------------------------- + +== ========================== ============================================ +\ minreal minimal realization; pole/zero cancellation +\ ss/sminreal structurally minimal realization +\* :func:`~modelsimp.hsvd` hankel singular values (state contributions) +\* :func:`~modelsimp.balred` reduced-order approximations of LTI models +\* :func:`~modelsimp.modred` model order reduction +== ========================== ============================================ + + +Compensator design +---------------------------------------------------------------------------- + +== ========================== ============================================ \* :func:`rlocus` evans root locus -\* :func:`place` pole placement +\* :func:`~statefbk.place` pole placement \ estim form estimator given estimator gain \ reg form regulator given state-feedback and estimator gains -\ -**LQR/LQG design** --------------------------------------------------------------------------------- +== ========================== ============================================ + + +LQR/LQG design +---------------------------------------------------------------------------- + +== ========================== ============================================ \ ss/lqg single-step LQG design -\* :func:`lqr` linear-Quadratic (LQ) state-feedback regulator +\* :func:`~statefbk.lqr` linear quadratic (LQ) state-fbk regulator \ dlqr discrete-time LQ state-feedback regulator -\ lqry lq regulator with output weighting +\ lqry LQ regulator with output weighting \ lqrd discrete LQ regulator for continuous plant -\ ss/lqi linear-Quadratic-Integral (LQI) controller +\ ss/lqi Linear-Quadratic-Integral (LQI) controller \ ss/kalman Kalman state estimator -\ ss/kalmd discrete Kalman estimator for continuous plant +\ ss/kalmd discrete Kalman estimator for cts plant \ ss/lqgreg build LQG regulator from LQ gain and Kalman estimator \ ss/lqgtrack build LQG servo-controller \ augstate augment output by appending states -\ -**State-space (SS) models** --------------------------------------------------------------------------------- -\* :func:`rss` random stable continuous-time state-space models -\* :func:`drss` random stable discrete-time state-space models +== ========================== ============================================ + + +State-space (SS) models +---------------------------------------------------------------------------- + +== ========================== ============================================ +\* :func:`rss` random stable cts-time state-space models +\* :func:`drss` random stable disc-time state-space models \ ss2ss state coordinate transformation \ canon canonical forms of state-space models -\* :func:`ctrb` controllability matrix -\* :func:`obsv` observability matrix -\* :func:`gram` controllability and observability gramians +\* :func:`~statefbk.ctrb` controllability matrix +\* :func:`~statefbk.obsv` observability matrix +\* :func:`~statefbk.gram` controllability and observability gramians \ ss/prescale optimal scaling of state-space models. \ balreal gramian-based input/output balancing \ ss/xperm reorder states. -\ -**Frequency response data (FRD) models** --------------------------------------------------------------------------------- +== ========================== ============================================ + + +Frequency response data (FRD) models +---------------------------------------------------------------------------- + +== ========================== ============================================ \ frd/chgunits change frequency vector units \ frd/fcat merge frequency responses \ frd/fselect select frequency range or subgrid \ frd/fnorm peak gain as a function of frequency -\ frd/abs entrywise magnitude of the frequency response +\ frd/abs entrywise magnitude of frequency response \ frd/real real part of the frequency response \ frd/imag imaginary part of the frequency response \ frd/interp interpolate frequency response data \ mag2db convert magnitude to decibels (dB) \ db2mag convert decibels (dB) to magnitude -\ -**Time delays** --------------------------------------------------------------------------------- +== ========================== ============================================ + + +Time delays +---------------------------------------------------------------------------- + +== ========================== ============================================ \ lti/hasdelay true for models with time delays \ lti/totaldelay total delay between each input/output pair \ lti/delay2z replace delays by poles at z=0 or FRD phase shift -\* :func:`pade` pade approximation of time delays -\ -**Model dimensions and characteristics** --------------------------------------------------------------------------------- +\* :func:`~delay.pade` pade approximation of time delays +== ========================== ============================================ + + +Model dimensions and characteristics +---------------------------------------------------------------------------- + +== ========================== ============================================ \ class model type ('tf', 'zpk', 'ss', or 'frd') \ isa test if model is of given type \ tf/size model sizes @@ -269,57 +328,95 @@ \ lti/issiso true for single-input/single-output models \ lti/isstable true for models with stable dynamics \ lti/reshape reshape array of linear models -\ -**Overloaded arithmetic operations** --------------------------------------------------------------------------------- -\* \+ and - add and subtract systems (parallel connection) +== ========================== ============================================ + +Overloaded arithmetic operations +---------------------------------------------------------------------------- + +== ========================== ============================================ +\* \+ and - add, subtract systems (parallel connection) \* \* multiply systems (series connection) -\ / right divide -- sys1/sys2 means sys1\*inv(sys2) -\- \\ left divide -- sys1\\sys2 means inv(sys1)\*sys2 +\ / right divide -- sys1\*inv(sys2) +\- \\ left divide -- inv(sys1)\*sys2 \ ^ powers of a given system \ ' pertransposition \ .' transposition of input/output map \ .\* element-by-element multiplication \ [..] concatenate models along inputs or outputs -\ lti/stack stack models/arrays along some array dimension +\ lti/stack stack models/arrays along some dimension \ lti/inv inverse of an LTI system \ lti/conj complex conjugation of model coefficients -\ -**Matrix equation solvers and linear algebra** --------------------------------------------------------------------------------- -\* lyap, dlyap solve Lyapunov equations +== ========================== ============================================ + +Matrix equation solvers and linear algebra +---------------------------------------------------------------------------- + +== ========================== ============================================ +\* :func:`~mateqn.lyap` solve continuous-time Lyapunov equations +\* :func:`~mateqn.dlyap` solve discrete-time Lyapunov equations \ lyapchol, dlyapchol square-root Lyapunov solvers -\* care, dare solve algebraic Riccati equations +\* :func:`~mateqn.care` solve continuous-time algebraic Riccati + equations +\* :func:`~mateqn.dare` solve disc-time algebraic Riccati equations \ gcare, gdare generalized Riccati solvers \ bdschur block diagonalization of a square matrix -\ -**Additional functions** --------------------------------------------------------------------------------- -\* :func:`gangof4` generate the Gang of 4 sensitivity plots -\* :func:`linspace` generate a set of numbers that are linearly +== ========================== ============================================ + + +Additional functions +---------------------------------------------------------------------------- + +== ========================== ============================================ +\* :func:`~freqplot.gangof4` generate the Gang of 4 sensitivity plots +\* :func:`~numpy.linspace` generate a set of numbers that are linearly spaced -\* :func:`logspace` generate a set of numbers that are +\* :func:`~numpy.logspace` generate a set of numbers that are logarithmically spaced -\* :func:`unwrap` unwrap a phase angle to give a continuous curve -== ========================== ================================================ +\* :func:`~ctrlutil.unwrap` unwrap phase angle to give continuous curve +== ========================== ============================================ + """ def ss(*args): """ Create a state space system. - + + The function accepts either 1 or 4 parameters: + + ``ss(sys)`` + Convert a linear system into space system form. Always creates a + new system, even if sys is already a StateSpace object. + + ``ss(A, B, C, D)`` + Create a state space system from the matrices of its state and + output equations: + + .. math:: + \dot x = A \cdot x + B \cdot u + + y = C \cdot x + D \cdot u + + The matrices can be given as *array like* data types or strings. + Everything that the constructor of :class:`numpy.matrix` accepts is + permissible here too. + Parameters ---------- - A: numpy matrix or matrix-like object - B: numpy matrix or matrix-like object - C: numpy matrix or matrix-like object - D: numpy matrix or matrix-like object - sys: StateSpace or TransferFunction object - ss accepts a set of `A`, `B`, `C`, `D` matrices or `sys`. + sys: Lti (StateSpace or TransferFunction) + A linear system + A: array_like or string + System matrix + B: array_like or string + Control matrix + C: array_like or string + Output matrix + D: array_like or string + Feed forward matrix Returns ------- - out: StateSpace object + out: StateSpace + The new linear system Raises ------ @@ -334,11 +431,15 @@ Examples -------- - >>> sys = ss(A, B, C, D) # Create a StateSpace object from these matrices. - >>> sys = ss(sys1) # Convert a TransferFunction to a StateSpace object. + >>> # Create a StateSpace object from four "matrices". + >>> sys1 = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") + + >>> # Convert a TransferFunction to a StateSpace object. + >>> sys_tf = tf([2.], [1., 3]) + >>> sys2 = ss(sys_tf) """ - + if len(args) == 4: return StateSpace(args[0], args[1], args[2], args[3]) elif len(args) == 1: @@ -353,20 +454,41 @@ else: raise ValueError("Needs 1 or 4 arguments; received %i." % len(args)) + def tf(*args): """ - Create a transfer function system. + Create a transfer function system. Can create MIMO systems. + + The function accepts either 1 or 2 parameters: + + ``tf(sys)`` + Convert a linear system into transfer function form. Always creates + a new system, even if sys is already a TransferFunction object. + + ``tf(num, den)`` + Create a transfer function system from its numerator and denominator + polynomial coefficients. + + If `num` and `den` are 1D array_like objects, the function creates a + SISO system. + To create a MIMO system, `num` and `den` need to be 2D nested lists + of array_like objects. (A 3 dimensional data structure in total.) + (For details see note below.) + Parameters ---------- - num: vector, or list of lists of vectors - den: vector, or list of lists of vectors - sys: StateSpace or TransferFunction object - tf accepts a `num` and `den`, or `sys``. + sys: Lti (StateSpace or TransferFunction) + A linear system + num: array_like, or list of list of array_like + Polynomial coefficients of the numerator + den: array_like, or list of list of array_like + Polynomial coefficients of the denominator Returns ------- - out: TransferFunction object + out: TransferFunction + The new linear system Raises ------ @@ -383,19 +505,34 @@ Notes -------- - `num`[`i`][`j`] is the vector of polynomial coefficients of the transfer - function numerator from the (`j`+1)st output to the (`i`+1)st input. - `den`[`i`][`j`] works the same way. + + .. todo:: + + The next paragraph contradicts the comment in the example! + Also "input" should come before "output" in the sentence: + + "from the (j+1)st output to the (i+1)st input" + + ``num[i][j]`` contains the polynomial coefficients of the numerator + for the transfer function from the (j+1)st output to the (i+1)st input. + ``den[i][j]`` works the same way. + + The coefficients ``[2, 3, 4]`` denote the polynomial + :math:`2 \cdot s^2 + 3 \cdot s + 4`. Examples -------- + >>> # Create a MIMO transfer function object + >>> # The transfer function from the 2nd input to the 1st output is + >>> # (3s + 4) / (6s^2 + 5s + 4). >>> num = [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]]] >>> den = [[[9., 8., 7.], [6., 5., 4.]], [[3., 2., 1.], [-1., -2., -3.]]] - >>> sys = tf(num, den) - The transfer function from the 2nd input to the 1st output is - (3s + 4) / (6s^2 + 5s + 4). - >>> sys = tf(sys1) # Convert a StateSpace to a TransferFunction object. + >>> sys1 = tf(num, den) + >>> # Convert a StateSpace to a TransferFunction object. + >>> sys_ss = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") + >>> sys2 = tf(sys1) + """ if len(args) == 2: @@ -412,22 +549,40 @@ else: raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) + def ss2tf(*args): """ Transform a state space system to a transfer function. + The function accepts either 1 or 4 parameters: + + ``ss2tf(sys)`` + Convert a linear system into space system form. Always creates a + new system, even if sys is already a StateSpace object. + + ``ss2tf(A, B, C, D)`` + Create a state space system from the matrices of its state and + output equations. + + For details see: :func:`ss` + Parameters ---------- - A: numpy matrix or matrix-like object - B: numpy matrix or matrix-like object - C: numpy matrix or matrix-like object - D: numpy matrix or matrix-like object - sys: StateSpace object - ss accepts a set of `A`, `B`, `C`, `D` matrices, or `sys`. + sys: StateSpace + A linear system + A: array_like or string + System matrix + B: array_like or string + Control matrix + C: array_like or string + Output matrix + D: array_like or string + Feed forward matrix Returns ------- - out: TransferFunction object + out: TransferFunction + New linear system in transfer function form Raises ------ @@ -445,8 +600,14 @@ Examples -------- - >>> sys = ss2tf(A, B, C, D) - >>> sys = ss2tf(sys1) # Convert a StateSpace to a TransferFunction object. + >>> A = [[1., -2], [3, -4]] + >>> B = [[5.], [7]] + >>> C = [[6., 8]] + >>> D = [[9.]] + >>> sys1 = ss2tf(A, B, C, D) + + >>> sys_ss = ss(A, B, C, D) + >>> sys2 = ss2tf(sys_ss) """ @@ -468,22 +629,37 @@ """ Transform a transfer function to a state space system. + The function accepts either 1 or 2 parameters: + + ``tf2ss(sys)`` + Convert a linear system into transfer function form. Always creates + a new system, even if sys is already a TransferFunction object. + + ``tf2ss(num, den)`` + Create a transfer function system from its numerator and denominator + polynomial coefficients. + + For details see: :func:`tf` + Parameters ---------- - num: vector, or list of lists of vectors - den: vector, or list of lists of vectors - sys: TransferFunction object - tf2ss accepts `num` and `den`, or `sys`. + sys: Lti (StateSpace or TransferFunction) + A linear system + num: array_like, or list of list of array_like + Polynomial coefficients of the numerator + den: array_like, or list of list of array_like + Polynomial coefficients of the denominator Returns ------- - out: StateSpace object + out: StateSpace + New linear system in state space form Raises ------ ValueError - if `num` and `den` have invalid or unequal dimensions, or if an invalid - number of arguments is passed in + if `num` and `den` have invalid or unequal dimensions, or if an + invalid number of arguments is passed in TypeError if `num` or `den` are of incorrect type, or if sys is not a TransferFunction object @@ -494,18 +670,14 @@ tf ss2tf - Notes - -------- - `num`[`i`][`j`] is the vector of polynomial coefficients of the transfer - function numerator from the (`j`+1)st output to the (`i`+1)st input. - `den`[`i`][`j`] works the same way. - Examples -------- >>> num = [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]]] >>> den = [[[9., 8., 7.], [6., 5., 4.]], [[3., 2., 1.], [-1., -2., -3.]]] - >>> sys = tf2ss(num, den) - >>> sys = tf2ss(sys1) # Convert a TransferFunction to a StateSpace object. + >>> sys1 = tf2ss(num, den) + + >>> sys_tf = tf(num, den) + >>> sys2 = tf2ss(sys_tf) """ @@ -523,17 +695,21 @@ def rss(states=1, inputs=1, outputs=1): """ - Create a stable continuous random state space object. + Create a stable **continuous** random state space object. Parameters ---------- states: integer + Number of state variables inputs: integer + Number of system inputs outputs: integer + Number of system outputs Returns ------- - sys: StateSpace object + sys: StateSpace + The randomly created linear system Raises ------ @@ -547,8 +723,8 @@ Notes ----- If the number of states, inputs, or outputs is not specified, then the - missing numbers are assumed to be 1. The poles of the returned system will - always have a negative real part. + missing numbers are assumed to be 1. The poles of the returned system + will always have a negative real part. """ @@ -556,17 +732,21 @@ def drss(states=1, inputs=1, outputs=1): """ - Create a stable discrete random state space object. + Create a stable **discrete** random state space object. Parameters ---------- states: integer + Number of state variables inputs: integer + Number of system inputs outputs: integer + Number of system outputs Returns ------- - sys: StateSpace object + sys: StateSpace + The randomly created linear system Raises ------ @@ -580,8 +760,8 @@ Notes ----- If the number of states, inputs, or outputs is not specified, then the - missing numbers are assumed to be 1. The poles of the returned system will - always have a magnitude less than 1. + missing numbers are assumed to be 1. The poles of the returned system + will always have a magnitude less than 1. """ @@ -589,15 +769,17 @@ def pole(sys): """ - Return system poles. + Compute system poles. Parameters ---------- - sys: StateSpace or TransferFunction object + sys: StateSpace or TransferFunction + Linear system Returns ------- poles: ndarray + Array that contains the system's poles. Raises ------ @@ -610,7 +792,8 @@ Notes ----- - This function is a wrapper for StateSpace.pole and TransferFunction.pole. + This function is a wrapper for StateSpace.pole and + TransferFunction.pole. """ @@ -618,15 +801,17 @@ def zero(sys): """ - Return system zeros. + Compute system zeros. Parameters ---------- - sys: StateSpace or TransferFunction object + sys: StateSpace or TransferFunction + Linear system Returns ------- zeros: ndarray + Array that contains the system's zeros. Raises ------ @@ -639,7 +824,8 @@ Notes ----- - This function is a wrapper for StateSpace.zero and TransferFunction.zero. + This function is a wrapper for StateSpace.zero and + TransferFunction.zero. """ @@ -651,8 +837,10 @@ Parameters ---------- - sys: StateSpace or TransferFunction object + sys: StateSpace or TransferFunction + Linear system omega: scalar + Frequency Returns ------- @@ -670,12 +858,12 @@ Examples -------- - >>> sys = rss(3, 2, 2) + >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") >>> evalfr(sys, 1.) - array([[ 4.09376126 -6.2171555j , 23.71332080-35.24245284j], - [ 0.83405186 -1.82896006j, 8.10962251-12.66640309j]]) - This is the transfer function matrix evaluated at s = i. + array([[ 44.8-21.4j]]) + >>> # This is the transfer function matrix evaluated at s = i. + .. todo:: Add example with MIMO system """ return sys.evalfr(omega) @@ -686,8 +874,10 @@ Parameters ---------- - sys: StateSpace or TransferFunction object - omega: list or ndarray + sys: StateSpace or TransferFunction + Linear system + omega: array_like + List of frequencies Returns ------- @@ -708,16 +898,26 @@ Examples -------- - >>> sys = rss(3, 2, 2) + >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") >>> mag, phase, omega = freqresp(sys, [0.1, 1., 10.]) - >>> mag[0, 1, :] - array([ 55.43747231, 42.47766549, 1.97225895]) - >>> phase[1, 0, :] - array([-0.12611087, -1.14294316, 2.5764547 ]) - This is the magnitude of the frequency response from the 2nd input to the - 1st output, and the phase (in radians) of the frequency response from the - 1st input to the 2nd output, for s = 0.1i, i, 10i. - + >>> mag + array([[[ 58.8576682 , 49.64876635, 13.40825927]]]) + >>> phase + array([[[-0.05408304, -0.44563154, -0.66837155]]]) + + .. todo:: + Add example with MIMO system + + #>>> sys = rss(3, 2, 2) + #>>> mag, phase, omega = freqresp(sys, [0.1, 1., 10.]) + #>>> mag[0, 1, :] + #array([ 55.43747231, 42.47766549, 1.97225895]) + #>>> phase[1, 0, :] + #array([-0.12611087, -1.14294316, 2.5764547 ]) + #>>> # This is the magnitude of the frequency response from the 2nd + #>>> # input to the 1st output, and the phase (in radians) of the + #>>> # frequency response from the 1st input to the 2nd output, for + #>>> # s = 0.1i, i, 10i. """ return sys.freqresp(omega) @@ -728,11 +928,17 @@ Examples -------- - >>> bode(sys) - >>> bode(sys, w) - >>> bode(sys1, sys2, ..., sysN) - >>> bode(sys1, sys2, ..., sysN, w) - >>> bode(sys1, 'plotstyle1', ..., sysN, 'plotstyleN') + >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") + >>> mag, phase, omega = bode(sys) + + .. todo:: + + Document these use cases + + * >>> bode(sys, w) + * >>> bode(sys1, sys2, ..., sysN) + * >>> bode(sys1, sys2, ..., sysN, w) + * >>> bode(sys1, 'plotstyle1', ..., sysN, 'plotstyleN') """ # If the first argument is a list, then assume python-control calling format @@ -799,13 +1005,17 @@ Parameters ---------- - sys: StateSpace or TransferFunction object - klist: optional list of gains + sys: StateSpace or TransferFunction + Linear system + klist: + optional list of gains Returns ------- - rlist: list of roots for each gain - klist: list of gains used to compute roots + rlist: + list of roots for each gain + klist: + list of gains used to compute roots """ from rlocus import RootLocus if (klist == None): @@ -818,26 +1028,35 @@ def margin(*args): """Calculate gain and phase margins and associated crossover frequencies - - Usage: - gm, pm, wg, wp = margin(sys) - gm, pm, wg, wp = margin(mag,phase,w) + Function ``margin`` takes either 1 or 3 parameters. + Parameters ---------- - sys : linsys + sys : StateSpace or TransferFunction Linear SISO system mag, phase, w : array_like - Input magnitude, phase (in deg.), and frequencies (rad/sec) from bode - frequency response data + Input magnitude, phase (in deg.), and frequencies (rad/sec) from + bode frequency response data Returns ------- gm, pm, wg, wp : float Gain margin gm, phase margin pm (in deg), and associated crossover frequencies wg and wp (in rad/sec) of SISO open-loop. If more than - one crossover frequency is detected, returns the lowest corresponding - margin. + one crossover frequency is detected, returns the lowest + corresponding margin. + + Examples + -------- + >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") + >>> gm, pm, wg, wp = margin(sys) + margin: no magnitude crossings found + + .. todo:: + better ecample system! + + #>>> gm, pm, wg, wp = margin(mag, phase, w) """ if len(args) == 1: sys = args[0] Modified: trunk/src/modelsimp.py =================================================================== --- trunk/src/modelsimp.py 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/src/modelsimp.py 2011-06-22 06:02:02 UTC (rev 162) @@ -47,17 +47,21 @@ from statesp import StateSpace # Hankel Singular Value Decomposition -# The following returns the Hankel singular values, which are singular values of the matrix formed by multiplying the controllability and observability grammians +# The following returns the Hankel singular values, which are singular values +#of the matrix formed by multiplying the controllability and observability +#grammians def hsvd(sys): - """Calculate the Hankel singular values + """Calculate the Hankel singular values. Parameters ---------- - sys : a state space system + sys : StateSpace + A state space system Returns ------- - H : a list of Hankel singular values + H : Matrix + A list of Hankel singular values See Also -------- @@ -65,7 +69,11 @@ Notes ----- - The Hankel singular values are the singular values of the Hankel operator. In practice, we compute the square root of the eigenvalues of the matrix formed by taking the product of the observability and controllability gramians. There are other (more efficient) methods based on solving the Lyapunov equation in a particular way (more details soon). + The Hankel singular values are the singular values of the Hankel operator. + In practice, we compute the square root of the eigenvalues of the matrix + formed by taking the product of the observability and controllability + gramians. There are other (more efficient) methods based on solving the + Lyapunov equation in a particular way (more details soon). Examples -------- @@ -84,29 +92,36 @@ # Return the Hankel singular values return hsv -def modred(sys,ELIM,method='matchdc'): - """Model reduction of sys by eliminating the states in ELIM using a given method +def modred(sys, ELIM, method='matchdc'): + """ + Model reduction of `sys` by eliminating the states in `ELIM` using a given + method. Parameters ---------- - sys: original system to reduce - ELIM: vector of states to eliminate - method: method of removing states in ELIM (truncate or matchdc) + sys: StateSpace + Original system to reduce + ELIM: array + Vector of states to eliminate + method: string + Method of removing states in `ELIM`: either ``'truncate'`` or + ``'matchdc'``. Returns ------- - rsys: a reduced order model + rsys: StateSpace + A reduced order model Raises ------ ValueError - if `method` is not either `matchdc` or `truncate` - if eigenvalues of `sys.A` are not all in left half plane (sys must be stable) + * if `method` is not either ``'matchdc'`` or ``'truncate'`` + * if eigenvalues of `sys.A` are not all in left half plane + (`sys` must be stable) Examples -------- - >>> rsys = modred(sys,ELIM,method='truncate') - + >>> rsys = modred(sys, ELIM, method='truncate') """ #Check for ss system object, need a utility for this? @@ -168,30 +183,38 @@ rsys = StateSpace(Ar,Br,Cr,Dr) return rsys -def balred(sys,orders,method='truncate'): - """Balanced reduced order model of sys of a given order. States are eliminated based on Hankel singular value. +def balred(sys, orders, method='truncate'): + """ + Balanced reduced order model of sys of a given order. + States are eliminated based on Hankel singular value. Parameters ---------- - sys: original system to reduce - orders: desired order of reduced order model (if a vector, returns a vector of systems) - method: method of removing states (truncate or matchdc) + sys: StateSpace + Original system to reduce + orders: integer or array of integer + Desired order of reduced order model (if a vector, returns a vector + of systems) + method: string + Method of removing states, either ``'truncate'`` or ``'matchdc'``. Returns ------- - rsys: a reduced order model + rsys: StateSpace + A reduced order model Raises ------ ValueError - if `method` is not `truncate` - if eigenvalues of `sys.A` are not all in left half plane (sys must be stable) + * if `method` is not ``'truncate'`` + * if eigenvalues of `sys.A` are not all in left half plane + (`sys` must be stable) ImportError if slycot routine ab09ad is not found Examples -------- - >>> rsys = balred(sys,order,method='truncate') + >>> rsys = balred(sys, order, method='truncate') """ @@ -233,39 +256,56 @@ return rsys -def era(YY,m,n,nin,nout,r): - """Calculate an ERA model of order r based on the impulse-response data YY - +def era(YY, m, n, nin, nout, r): + """ + Calculate an ERA model of order `r` based on the impulse-response data `YY`. + + .. note:: This function is not implemented yet. + Parameters ---------- - YY: nout x nin dimensional impulse-response data - m: number of rows in Hankel matrix - n: number of columns in Hankel matrix - nin: number of input variables - nout: number of output variables - r: order of model + YY: array + `nout` x `nin` dimensional impulse-response data + m: integer + Number of rows in Hankel matrix + n: integer + Number of columns in Hankel matrix + nin: integer + Number of input variables + nout: integer + Number of output variables + r: integer + Order of model Returns ------- - sys: a reduced order model sys=ss(Ar,Br,Cr,Dr) + sys: StateSpace + A reduced order model sys=ss(Ar,Br,Cr,Dr) Examples -------- - >>> rsys = era(YY,m,n,nin,nout,r) + >>> rsys = era(YY, m, n, nin, nout, r) + """ + raise NotImplementedError('This function is not implemented yet.') +def markov(Y, U, M): """ -def markov(Y,U,M): - """Calculate the first M Markov parameters [D CB CAB ...] from input U, output Y + Calculate the first `M` Markov parameters [D CB CAB ...] + from input `U`, output `Y`. Parameters ---------- - Y: output data - U: input data - M: number of Markov parameters to output + Y: array_like + Output data + U: array_like + Input data + M: integer + Number of Markov parameters to output Returns ------- - H: first M Markov parameters + H: matrix + First M Markov parameters Notes ----- @@ -273,8 +313,7 @@ Examples -------- - >>> H = markov(Y,U,M) - + >>> H = markov(Y, U, M) """ # Convert input parameters to matrices (if they aren't already) Modified: trunk/src/nichols.py =================================================================== --- trunk/src/nichols.py 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/src/nichols.py 2011-06-22 06:02:02 UTC (rev 162) @@ -49,23 +49,19 @@ def nichols(syslist, omega=None, grid=True): """Nichols plot for a system - Usage - ===== - magh = nichols(sys, omega=None) - Plots a Nichols plot for the system over a (optional) frequency range. Parameters ---------- - syslist : linsys + syslist : list of Lti, or Lti List of linear input/output systems (single system is OK) - omega : freq_range + omega : array_like Range of frequencies (list or bounds) in rad/sec grid : boolean, optional True if the plot should include a Nichols-chart grid. Default is True. - Return values - ------------- + Returns + ------- None """ Modified: trunk/src/pzmap.py =================================================================== --- trunk/src/pzmap.py 2011-06-18 19:13:09 UTC (rev 161) +++ trunk/src/pzmap.py 2011-06-22 06:02:02 UTC (rev 162) @@ -41,27 +41,54 @@ # $Id:pzmap.py 819 2009-05-29 21:28:07Z murray $ import matplotlib.pyplot as plt -import scipy as sp -import numpy as np -import xferfcn +#import scipy as sp +#import numpy as np +from numpy import real, imag +from lti import Lti -# Compute poles and zeros for a system -#! TODO: extend this to handle state space systems -def pzmap(sys, Plot=True): - """Plot a pole/zero map for a transfer function""" - if (isinstance(sys, xferfcn.TransferFunction)): - poles = sp.roots(np.squeeze(np.asarray(sys.den))); - zeros = sp.roots(np.squeeze(np.asarray(sys.num))); - else: - raise NotImplementedError("pzmap not implemented for state space systems yet.") +# TODO: Implement more elegant cross-style axes. See: +# http://matplotlib.sourceforge.net/examples/axes_grid/demo_axisline_style.html +# http://matplotlib.sourceforge.net/examples/axes_grid/demo_curvelinear_grid.html +def pzmap(sys, Plot=True, title='Pole Zero Map'): + """ + Plot a pole/zero map for a linear system. + + Parameters + ---------- + sys: Lti (StateSpace or TransferFunction) + Linear system for which poles and zeros are computed. + Plot: bool + If ``True`` a graph is generated with Matplotlib, + otherwise the poles and zeros are only computed and returned. + + Returns + ------- + pole: array + The systems poles + zeros: array + The system's zeros. + """ + if not isinstance(sys, Lti): + raise TypeError('Argument ``sys``: must be a linear system.') + + poles = sys.pole() + zeros = sys.zero() if (Plot): # Plot the locations of the poles and zeros - plt.plot(sp.real(poles), sp.imag(poles), 'x'); plt.hold(True); - plt.plot(sp.real(zeros), sp.imag(zeros), 'o'); - + if len(poles) > 0: + plt.scatter(real(poles), imag(poles), s=50, marker='x') + if len(zeros) > 0: + plt.scat... [truncated message content] |
From: <mur...@us...> - 2011-06-22 19:26:08
|
Revision: 164 http://python-control.svn.sourceforge.net/python-control/?rev=164&view=rev Author: murrayrm Date: 2011-06-22 19:26:01 +0000 (Wed, 22 Jun 2011) Log Message: ----------- Fixed up some typos in documentation + instructions for copying to sf.net Modified Paths: -------------- trunk/ChangeLog trunk/doc/README trunk/doc/intro.rst Removed Paths: ------------- trunk/doc/control.tex Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-06-22 18:59:13 UTC (rev 163) +++ trunk/ChangeLog 2011-06-22 19:26:01 UTC (rev 164) @@ -1,5 +1,11 @@ 2011-06-22 Richard Murray <murray@malabar.local> + * doc/intro.rst: fixed some small types + + * doc/control.tex: removed (no longer needed) + +2011-06-22 Richard Murray <murray@malabar.local> + * doc/intro.rst: Added a slightly more general introduction, with a pointer to the python-control wiki (on sf.net) Modified: trunk/doc/README =================================================================== --- trunk/doc/README 2011-06-22 18:59:13 UTC (rev 163) +++ trunk/doc/README 2011-06-22 19:26:01 UTC (rev 164) @@ -13,3 +13,8 @@ 4. >> touch *.rst >> make html [or make latex] + +Creating/updating manual on sourceforge: + +5. >> rsync -rav _build/html/ us...@sh...:/home/project-web/python-control/htdocs/manual-N.mx/ + Deleted: trunk/doc/control.tex =================================================================== --- trunk/doc/control.tex 2011-06-22 18:59:13 UTC (rev 163) +++ trunk/doc/control.tex 2011-06-22 19:26:01 UTC (rev 164) @@ -1,21 +0,0 @@ -\documentclass{manual} - -\release{0.1} - -\begin{document} -\sectionauthor{Richard Murray}{mu...@cd...} - -\section{\module{control}} -This is some information on the control module. - -\section{Differences from MATLAB} -\begin{itemize} - \item You must include commas in vectors. So [1 2 3] must be [1, 2, 3]. - \item Functions that return multiple arguments use tuples - \item Can't use braces for collections; use tuples instead - \item Transfer functions are only implemented for SISO systems (due - to limitations in the underlying signals.lti class); use state space - representations for MIMO systems. -\end{itemize} - -\end{document} Modified: trunk/doc/intro.rst =================================================================== --- trunk/doc/intro.rst 2011-06-22 18:59:13 UTC (rev 163) +++ trunk/doc/intro.rst 2011-06-22 19:26:01 UTC (rev 164) @@ -13,7 +13,7 @@ the commands available in the MATLAB Control Systems Toolbox. In addition to the documentation here, there is a project wiki that -contains some addional informaiton about how to use the package +contains some additional information about how to use the package (including some detailed worked examples): http://python-control.sourceforge.net This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2011-07-02 01:55:56
|
Revision: 166 http://python-control.svn.sourceforge.net/python-control/?rev=166&view=rev Author: murrayrm Date: 2011-07-02 01:55:50 +0000 (Sat, 02 Jul 2011) Log Message: ----------- * Added link to numpy vs MATLAB page in scipy docs * Fixed a problem with import statements in rlocus (overwrote statesp.place) Modified Paths: -------------- trunk/ChangeLog trunk/doc/intro.rst trunk/src/__init__.py trunk/src/rlocus.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-06-26 02:44:09 UTC (rev 165) +++ trunk/ChangeLog 2011-07-02 01:55:50 UTC (rev 166) @@ -1,3 +1,13 @@ +2011-07-01 Richard Murray <murray@malabar.local> + + * src/rlocus.py: modified scipy import to only import those + functions that we actually use. This fixes a problem pointed out by + Carsten Knoll (TU Dresden) where control.place could be overwritten + by numpy.place (because of an "from scipy import *" in rlocus.py + + * doc/intro.rst: Added link to scipy web page talking about the + differences between numpy and MATLAB (contributed by Shuo Han). + 2011-06-25 Richard Murray <murray@malabar.local> * src/xferfcn.py (TransferFunction._common_den): changed tolerance Modified: trunk/doc/intro.rst =================================================================== --- trunk/doc/intro.rst 2011-06-26 02:44:09 UTC (rev 165) +++ trunk/doc/intro.rst 2011-07-02 01:55:50 UTC (rev 166) @@ -21,6 +21,13 @@ Some Differences from MATLAB ---------------------------- +The python-control package makes use of NumPy and SciPy. A list of +general differences between NumPy and MATLAB can be found here: + + http://www.scipy.org/NumPy_for_Matlab_Users + +In terms of the python-control package more specifically, here are +some thing to keep in mind: * You must include commas in vectors. So [1 2 3] must be [1, 2, 3]. * Functions that return multiple arguments use tuples * Can't use braces for collections; use tuples instead @@ -30,7 +37,7 @@ Getting Started --------------- -1. Download the latest release from http:sf.net/projects/python-control/files. +1. Download latest release from http://sf.net/projects/python-control/files. 2. Untar the source code in a temporary directory and run 'python setup.py install' to build and install the code 3. To see if things are working correctly, run ipython -pylab and run the @@ -38,8 +45,8 @@ Bode plot and Nyquist plot for a simple second order system. 4. To see the commands that are available, run the following commands in ipython:: - >>> import control - >>> ?control.matlab + >>> import control + >>> ?control.matlab 5. If you want to have a MATLAB-like environment for running the control toolbox, use:: - >>> from control.matlab import * + >>> from control.matlab import * Modified: trunk/src/__init__.py =================================================================== --- trunk/src/__init__.py 2011-06-26 02:44:09 UTC (rev 165) +++ trunk/src/__init__.py 2011-07-02 01:55:50 UTC (rev 166) @@ -56,6 +56,7 @@ """ # Import functions from within the control system library +#! Should probably only import the exact functions we use... from xferfcn import * from statesp import * from freqplot import * Modified: trunk/src/rlocus.py =================================================================== --- trunk/src/rlocus.py 2011-06-26 02:44:09 UTC (rev 165) +++ trunk/src/rlocus.py 2011-07-02 01:55:50 UTC (rev 166) @@ -46,7 +46,7 @@ # $Id$ # Packages used by this module -from scipy import * +from scipy import array, poly1d, row_stack, zeros_like import scipy.signal # signal processing toolbox import pylab # plotting routines import xferfcn # transfer function manipulation This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2011-07-13 16:03:38
|
Revision: 167 http://python-control.svn.sourceforge.net/python-control/?rev=167&view=rev Author: murrayrm Date: 2011-07-13 16:03:32 +0000 (Wed, 13 Jul 2011) Log Message: ----------- * Updated output arguments for matlab.py time response functions to match MATLAB conventions (they were switched) * Added long version names for frequency plots: BodePlot, NyquistPlot, etc * Fixed some import errors in rlocus.py Details in ChangeLog. Modified Paths: -------------- trunk/ChangeLog trunk/src/freqplot.py trunk/src/matlab.py trunk/src/rlocus.py trunk/tests/matlab_test.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-07-02 01:55:50 UTC (rev 166) +++ trunk/ChangeLog 2011-07-13 16:03:32 UTC (rev 167) @@ -1,3 +1,18 @@ +2011-07-11 Richard Murray <murray@malabar.local> + + * src/rlocus.py: added real() and imag() to list of functions + imported from numpy + + * src/freqplot.py: renamed plotting functions to BodePlot, + NyquistPlot, GangOf4Plot and MarginPlot. Set up aliases to the more + common names (bode, nyquest, gangof4, margin). Mainly playing + around with idea for the eventual interface to use. + + * tests/matlab_test.py: updated timeresp outputs to match MATLAB + + * src/matlab.py (impulse, initial, lsim, step): switched outputs + from step, impulse, initial, lsim to match MATLAB standard + 2011-07-01 Richard Murray <murray@malabar.local> * src/rlocus.py: modified scipy import to only import those Modified: trunk/src/freqplot.py =================================================================== --- trunk/src/freqplot.py 2011-07-02 01:55:50 UTC (rev 166) +++ trunk/src/freqplot.py 2011-07-13 16:03:32 UTC (rev 167) @@ -55,7 +55,7 @@ # # Bode plot -def bode(syslist, omega=None, dB=False, Hz=False, deg=True, +def BodePlot(syslist, omega=None, dB=False, Hz=False, deg=True, color=None, Plot=True): """Bode plot for a system @@ -178,7 +178,7 @@ return mags, phases, omegas # Nyquist plot -def nyquist(syslist, omega=None, Plot=True): +def NyquistPlot(syslist, omega=None, Plot=True): """Nyquist plot for a system Plots a Nyquist plot for the system over a (optional) frequency range. @@ -245,7 +245,7 @@ # Gang of Four #! TODO: think about how (and whether) to handle lists of systems -def gangof4(P, C, omega=None): +def GangOf4Plot(P, C, omega=None): """Plot the "Gang of 4" transfer functions for a system Generates a 2x2 plot showing the "Gang of 4" sensitivity functions @@ -302,7 +302,7 @@ # gain and phase margins # contributed by Sawyer B. Fuller <mi...@ca...> -def margin(sysdata, deg=True): +def MarginPlot(sysdata, deg=True): """Calculate gain and phase margins and associated crossover frequencies Usage: @@ -457,3 +457,8 @@ return omega +# Function aliases +bode = BodePlot +nyquist = NyquistPlot +gangof4 = GangOf4Plot +margin = MarginPlot Modified: trunk/src/matlab.py =================================================================== --- trunk/src/matlab.py 2011-07-02 01:55:50 UTC (rev 166) +++ trunk/src/matlab.py 2011-07-13 16:03:32 UTC (rev 167) @@ -1165,12 +1165,12 @@ Returns ------- + yout: array + Response of the system + T: array Time values of the output - yout: array - Response of the system - See Also -------- lsim, initial, impulse @@ -1181,7 +1181,7 @@ ''' T, yout = timeresp.StepResponse(sys, T, X0, input, output, transpose = True, **keywords) - return T, yout + return yout, T def impulse(sys, T=None, input=0, output=0, **keywords): ''' @@ -1216,10 +1216,10 @@ Returns ------- + yout: array + Response of the system T: array Time values of the output - yout: array - Response of the system See Also -------- @@ -1231,7 +1231,7 @@ ''' T, yout = timeresp.ImpulseResponse(sys, T, 0, input, output, transpose = True, **keywords) - return T, yout + return yout, T def initial(sys, T=None, X0=0., input=0, output=0, **keywords): ''' @@ -1272,10 +1272,10 @@ Returns ------- + yout: array + Response of the system T: array Time values of the output - yout: array - Response of the system See Also -------- @@ -1287,7 +1287,7 @@ ''' T, yout = timeresp.InitialResponse(sys, T, X0, input, output, transpose = True, **keywords) - return T, yout + return yout, T def lsim(sys, U=0., T=None, X0=0., **keywords): ''' @@ -1324,10 +1324,10 @@ Returns ------- + yout: array + Response of the system. T: array Time values of the output. - yout: array - Response of the system. xout: array Time evolution of the state vector. @@ -1341,4 +1341,4 @@ ''' T, yout, xout = timeresp.ForcedResponse(sys, T, U, X0, transpose = True, **keywords) - return T, yout, xout + return yout, T, xout Modified: trunk/src/rlocus.py =================================================================== --- trunk/src/rlocus.py 2011-07-02 01:55:50 UTC (rev 166) +++ trunk/src/rlocus.py 2011-07-13 16:03:32 UTC (rev 167) @@ -46,7 +46,7 @@ # $Id$ # Packages used by this module -from scipy import array, poly1d, row_stack, zeros_like +from scipy import array, poly1d, row_stack, zeros_like, real, imag import scipy.signal # signal processing toolbox import pylab # plotting routines import xferfcn # transfer function manipulation Modified: trunk/tests/matlab_test.py =================================================================== --- trunk/tests/matlab_test.py 2011-07-02 01:55:50 UTC (rev 166) +++ trunk/tests/matlab_test.py 2011-07-13 16:03:32 UTC (rev 167) @@ -95,24 +95,24 @@ t = np.linspace(0, 1, 10) youttrue = np.array([9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, 42.3227, 44.9694, 47.1599, 48.9776]) - tout, yout = step(sys, T=t) + yout, tout = step(sys, T=t) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) # Play with arguments - tout, yout = step(sys, T=t, X0=0) + yout, tout = step(sys, T=t, X0=0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) X0 = np.array([0, 0]); - tout, yout = step(sys, T=t, X0=X0) + yout, tout = step(sys, T=t, X0=X0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) #Test MIMO system, which contains ``siso_ss1`` twice sys = self.mimo_ss1 - _t, y_00 = step(sys, T=t, input=0, output=0) - _t, y_11 = step(sys, T=t, input=1, output=1) + y_00, _t = step(sys, T=t, input=0, output=0) + y_11, _t = step(sys, T=t, input=1, output=1) np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) @@ -122,14 +122,14 @@ t = np.linspace(0, 1, 10) youttrue = np.array([86., 70.1808, 57.3753, 46.9975, 38.5766, 31.7344, 26.1668, 21.6292, 17.9245, 14.8945]) - tout, yout = impulse(sys, T=t) + yout, tout = impulse(sys, T=t) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) #Test MIMO system, which contains ``siso_ss1`` twice sys = self.mimo_ss1 - _t, y_00 = impulse(sys, T=t, input=0, output=0) - _t, y_11 = impulse(sys, T=t, input=1, output=1) + y_00, _t = impulse(sys, T=t, input=0, output=0) + y_11, _t = impulse(sys, T=t, input=1, output=1) np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) @@ -140,15 +140,15 @@ x0 = np.matrix(".5; 1.") youttrue = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, 1.1508, 0.5833, 0.1645, -0.1391]) - tout, yout = initial(sys, T=t, X0=x0) + yout, tout = initial(sys, T=t, X0=x0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) #Test MIMO system, which contains ``siso_ss1`` twice sys = self.mimo_ss1 x0 = np.matrix(".5; 1.; .5; 1.") - _t, y_00 = initial(sys, T=t, X0=x0, input=0, output=0) - _t, y_11 = initial(sys, T=t, X0=x0, input=1, output=1) + y_00, _t = initial(sys, T=t, X0=x0, input=0, output=0) + y_11, _t = initial(sys, T=t, X0=x0, input=1, output=1) np.testing.assert_array_almost_equal(y_00, youttrue, decimal=4) np.testing.assert_array_almost_equal(y_11, youttrue, decimal=4) @@ -160,10 +160,10 @@ u = np.array([1., 1, 1, 1, 1, 1, 1, 1, 1, 1]) youttrue = np.array([9., 17.6457, 24.7072, 30.4855, 35.2234, 39.1165, 42.3227, 44.9694, 47.1599, 48.9776]) - tout, yout, _xout = lsim(self.siso_ss1, u, t) + yout, tout, _xout = lsim(self.siso_ss1, u, t) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) np.testing.assert_array_almost_equal(tout, t) - _t, yout, _xout = lsim(self.siso_tf3, u, t) + yout, _t, _xout = lsim(self.siso_tf3, u, t) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) #test with initial value and special algorithm for ``U=0`` @@ -171,7 +171,7 @@ x0 = np.matrix(".5; 1.") youttrue = np.array([11., 8.1494, 5.9361, 4.2258, 2.9118, 1.9092, 1.1508, 0.5833, 0.1645, -0.1391]) - _t, yout, _xout = lsim(self.siso_ss1, u, t, x0) + yout, _t, _xout = lsim(self.siso_ss1, u, t, x0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) #Test MIMO system, which contains ``siso_ss1`` twice @@ -184,7 +184,7 @@ [1.9092, 39.1165], [1.1508, 42.3227], [0.5833, 44.9694], [0.1645, 47.1599], [-0.1391, 48.9776]]) - _t, yout, _xout = lsim(self.mimo_ss1, u, t, x0) + yout, _t, _xout = lsim(self.mimo_ss1, u, t, x0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) def testDcgain(self): @@ -206,7 +206,7 @@ #Compute the gain with a long simulation t = linspace(0, 1000, 1000) - _t, y = step(sys_ss, t) + y, _t = step(sys_ss, t) gain_sim = y[-1] print 'gain_sim:', gain_sim This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2011-07-16 06:05:52
|
Revision: 168 http://python-control.svn.sourceforge.net/python-control/?rev=168&view=rev Author: murrayrm Date: 2011-07-16 06:05:46 +0000 (Sat, 16 Jul 2011) Log Message: ----------- * Moved margin command from freqplot.py to margins.py (new file); updated matlab.py, init.py, etc to reflect changes * Added PhaseCrossoverFrequencies() function; contributed by Steffen Waldherr * Added initial unit tests for StabilityMargins(), PhaseCrossoverFrequencies() Modified Paths: -------------- trunk/ChangeLog trunk/src/__init__.py trunk/src/freqplot.py trunk/src/matlab.py trunk/src/statesp.py trunk/tests/matlab_test.py Added Paths: ----------- trunk/src/margins.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-07-13 16:03:32 UTC (rev 167) +++ trunk/ChangeLog 2011-07-16 06:05:46 UTC (rev 168) @@ -1,3 +1,24 @@ +2011-07-15 Richard Murray <murray@malabar.local> + + * tests/matlab_test.py (TestMatlab): added unittest for margin() + commands (calling format only) + + * src/statesp.py (StateSpace): updated comments + + * tests/margin_test.py: set up unit tests for StabilityMargins() and + PhaseCrossoverFrequencies() + + * src/__init__.py: added margins.py to __init__ + +2011-07-14 Richard Murray <murray@malabar.local> + + * src/margins.py (GainPhaseMargin): moved freqplot.MarginPlot to + margin.StabilityMargins (MarginPlot didn't actually plot anything) + + * src/margins.py (PhaseCrossoverFrequencies): added new function to + compute frequencies that we cross real axis. Contributed by Steffen + Waldherr <wal...@is...> + 2011-07-11 Richard Murray <murray@malabar.local> * src/rlocus.py: added real() and imag() to list of functions Modified: trunk/src/__init__.py =================================================================== --- trunk/src/__init__.py 2011-07-13 16:03:32 UTC (rev 167) +++ trunk/src/__init__.py 2011-07-16 06:05:46 UTC (rev 168) @@ -57,15 +57,16 @@ # Import functions from within the control system library #! Should probably only import the exact functions we use... -from xferfcn import * -from statesp import * -from freqplot import * -from nichols import * from bdalg import * -from statefbk import * from delay import * +from freqplot import * +from margins import * +from mateqn import * from modelsimp import * +from nichols import * from rlocus import * -from mateqn import * +from statefbk import * +from statesp import * from timeresp import ForcedResponse, InitialResponse, StepResponse, \ ImpulseResponse +from xferfcn import * Modified: trunk/src/freqplot.py =================================================================== --- trunk/src/freqplot.py 2011-07-13 16:03:32 UTC (rev 167) +++ trunk/src/freqplot.py 2011-07-16 06:05:46 UTC (rev 168) @@ -299,101 +299,6 @@ phase = np.squeeze(phase_tmp) plt.subplot(224); plt.loglog(omega, mag); - -# gain and phase margins -# contributed by Sawyer B. Fuller <mi...@ca...> -def MarginPlot(sysdata, deg=True): - """Calculate gain and phase margins and associated crossover frequencies - - Usage: - - gm, pm, sm, wg, wp, ws = margin(sysdata, deg=True) - - Parameters - ---------- - sysdata: linsys or (mag, phase, omega) sequence - sys : linsys - Linear SISO system - mag, phase, omega : sequence of array_like - Input magnitude, phase, and frequencies (rad/sec) sequence from - bode frequency response data - deg=True: boolean - If true, all input and output phases in degrees, else in radians - - Returns - ------- - gm, pm, sm, wg, wp, ws: float - Gain margin gm, phase margin pm, stability margin sm, and - associated crossover - frequencies wg, wp, and ws of SISO open-loop. If more than - one crossover frequency is detected, returns the lowest corresponding - margin. - """ - #TODO do this precisely without the effects of discretization of frequencies? - #TODO assumes SISO - #TODO unit tests, margin plot - - if (not getattr(sysdata, '__iter__', False)): - sys = sysdata - mag, phase, omega = bode(sys, deg=deg, Plot=False) - elif len(sysdata) == 3: - mag, phase, omega = sysdata - else: - raise ValueError("Margin sysdata must be either a linear system or a 3-sequence of mag, phase, omega.") - - if deg: - cycle = 360. - crossover = 180. - else: - cycle = 2 * np.pi - crossover = np.pi - - wrapped_phase = -np.mod(phase, cycle) - - # phase margin from minimum phase among all gain crossovers - neg_mag_crossings_i = np.nonzero(np.diff(mag < 1) > 0)[0] - mag_crossings_p = wrapped_phase[neg_mag_crossings_i] - if len(neg_mag_crossings_i) == 0: - if mag[0] < 1: # gain always less than one - wp = np.nan - pm = np.inf - else: # gain always greater than one - print "margin: no magnitude crossings found" - wp = np.nan - pm = np.nan - else: - min_mag_crossing_i = neg_mag_crossings_i[np.argmin(mag_crossings_p)] - wp = omega[min_mag_crossing_i] - pm = crossover + phase[min_mag_crossing_i] - if pm < 0: - print "warning: system unstable: negative phase margin" - - # gain margin from minimum gain margin among all phase crossovers - neg_phase_crossings_i = np.nonzero(np.diff(wrapped_phase < -crossover) > 0)[0] - neg_phase_crossings_g = mag[neg_phase_crossings_i] - if len(neg_phase_crossings_i) == 0: - wg = np.nan - gm = np.inf - else: - min_phase_crossing_i = neg_phase_crossings_i[ - np.argmax(neg_phase_crossings_g)] - wg = omega[min_phase_crossing_i] - gm = abs(1/mag[min_phase_crossing_i]) - if gm < 1: - print "warning: system unstable: gain margin < 1" - - # stability margin from minimum abs distance from -1 point - if deg: - phase_rad = phase * np.pi / 180. - else: - phase_rad = phase - L = mag * np.exp(1j * phase_rad) # complex loop response to -1 pt - min_Lplus1_i = np.argmin(np.abs(L + 1)) - sm = np.abs(L[min_Lplus1_i] + 1) - ws = phase[min_Lplus1_i] - - return gm, pm, sm, wg, wp, ws - # # Utility functions # @@ -461,4 +366,3 @@ bode = BodePlot nyquist = NyquistPlot gangof4 = GangOf4Plot -margin = MarginPlot Added: trunk/src/margins.py =================================================================== --- trunk/src/margins.py (rev 0) +++ trunk/src/margins.py 2011-07-16 06:05:46 UTC (rev 168) @@ -0,0 +1,193 @@ +"""margin.py + +Functions for computing stability margins and related functions. + +Routeins in this module: + +margin.StabilityMargins +margin.PhaseCrossoverFrequencies +""" + +"""Copyright (c) 2011 by California Institute of Technology +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the California Institute of Technology nor + the names of its contributors may be used to endorse or promote + products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +Author: Richard M. Murray +Date: 14 July 2011 + +$Id: xferfcn.py 165 2011-06-26 02:44:09Z murrayrm $ + +""" + +import xferfcn +from freqplot import bode +import numpy as np + +# gain and phase margins +# contributed by Sawyer B. Fuller <mi...@ca...> +#! TODO - need to add unit test functions +def StabilityMargins(sysdata, deg=True): + """Calculate gain, phase and stability margins and associated + crossover frequencies. + + Usage: + + gm, pm, sm, wg, wp, ws = StabilityMargins(sysdata, deg=True) + + Parameters + ---------- + sysdata: linsys or (mag, phase, omega) sequence + sys : linsys + Linear SISO system + mag, phase, omega : sequence of array_like + Input magnitude, phase, and frequencies (rad/sec) sequence from + bode frequency response data + deg=True: boolean + If true, all input and output phases in degrees, else in radians + + Returns + ------- + gm, pm, sm, wg, wp, ws: float + Gain margin gm, phase margin pm, stability margin sm, and + associated crossover + frequencies wg, wp, and ws of SISO open-loop. If more than + one crossover frequency is detected, returns the lowest corresponding + margin. + """ + #TODO do this precisely without the effects of discretization of frequencies? + #TODO assumes SISO + #TODO unit tests, margin plot + + if (not getattr(sysdata, '__iter__', False)): + sys = sysdata + mag, phase, omega = bode(sys, deg=deg, Plot=False) + elif len(sysdata) == 3: + mag, phase, omega = sysdata + else: + raise ValueError("Margin sysdata must be either a linear system or a 3-sequence of mag, phase, omega.") + + if deg: + cycle = 360. + crossover = 180. + else: + cycle = 2 * np.pi + crossover = np.pi + + wrapped_phase = -np.mod(phase, cycle) + + # phase margin from minimum phase among all gain crossovers + neg_mag_crossings_i = np.nonzero(np.diff(mag < 1) > 0)[0] + mag_crossings_p = wrapped_phase[neg_mag_crossings_i] + if len(neg_mag_crossings_i) == 0: + if mag[0] < 1: # gain always less than one + wp = np.nan + pm = np.inf + else: # gain always greater than one + print "margin: no magnitude crossings found" + wp = np.nan + pm = np.nan + else: + min_mag_crossing_i = neg_mag_crossings_i[np.argmin(mag_crossings_p)] + wp = omega[min_mag_crossing_i] + pm = crossover + phase[min_mag_crossing_i] + if pm < 0: + print "warning: system unstable: negative phase margin" + + # gain margin from minimum gain margin among all phase crossovers + neg_phase_crossings_i = np.nonzero(np.diff(wrapped_phase < -crossover) > 0)[0] + neg_phase_crossings_g = mag[neg_phase_crossings_i] + if len(neg_phase_crossings_i) == 0: + wg = np.nan + gm = np.inf + else: + min_phase_crossing_i = neg_phase_crossings_i[ + np.argmax(neg_phase_crossings_g)] + wg = omega[min_phase_crossing_i] + gm = abs(1/mag[min_phase_crossing_i]) + if gm < 1: + print "warning: system unstable: gain margin < 1" + + # stability margin from minimum abs distance from -1 point + if deg: + phase_rad = phase * np.pi / 180. + else: + phase_rad = phase + L = mag * np.exp(1j * phase_rad) # complex loop response to -1 pt + min_Lplus1_i = np.argmin(np.abs(L + 1)) + sm = np.abs(L[min_Lplus1_i] + 1) + ws = phase[min_Lplus1_i] + + return gm, pm, sm, wg, wp, ws + +# Contributed by Steffen Waldherr <wal...@is...> +#! TODO - need to add test functions +def PhaseCrossoverFrequencies(sys): + """ + Compute frequencies and gains at intersections with real axis + in Nyquist plot. + + Call as: + omega, gain = PhaseCrossoverFrequencies() + + Returns + ------- + omega: 1d array of (non-negative) frequencies where Nyquist plot + intersects the real axis + + gain: 1d array of corresponding gains + + Examples + -------- + >>> tf = TransferFunction([1], [1, 2, 3, 4]) + >>> PhaseCrossoverFrequenies(tf) + (array([ 1.73205081, 0. ]), array([-0.5 , 0.25])) + """ + + # Convert to a transfer function + tf = xferfcn._convertToTransferFunction(sys) + + # if not siso, fall back to (0,0) element + #! TODO: should add a check and warning here + num = tf.num[0][0] + den = tf.den[0][0] + + # Compute frequencies that we cross over the real axis + numj = (1.j)**np.arange(len(num)-1,-1,-1)*num + denj = (-1.j)**np.arange(len(den)-1,-1,-1)*den + allfreq = np.roots(np.imag(np.polymul(numj,denj))) + realfreq = np.real(allfreq[np.isreal(allfreq)]) + realposfreq = realfreq[realfreq >= 0.] + + # using real() to avoid rounding errors and results like 1+0j + # it would be nice to have a vectorized version of self.evalfr here + gain = np.real(np.asarray([tf.evalfr(f)[0][0] for f in realposfreq])) + + return realposfreq, gain Modified: trunk/src/matlab.py =================================================================== --- trunk/src/matlab.py 2011-07-13 16:03:32 UTC (rev 167) +++ trunk/src/matlab.py 2011-07-16 06:05:46 UTC (rev 168) @@ -72,6 +72,7 @@ import ctrlutil import freqplot import timeresp +import margins from statesp import StateSpace, _rss_generate, _convertToStateSpace from xferfcn import TransferFunction, _convertToTransferFunction from lti import Lti #base class of StateSpace, TransferFunction @@ -1025,7 +1026,6 @@ rlist = RootLocus(sys, klist, **keywords) return rlist, klist - def margin(*args): """Calculate gain and phase margins and associated crossover frequencies @@ -1060,16 +1060,15 @@ """ if len(args) == 1: sys = args[0] - margins = freqplot.margin(sys) + margin = margins.StabilityMargins(sys) elif len(args) == 3: - margins = freqplot.margin(args) + margin = margins.StabilityMargins(args) else: raise ValueError("Margin needs 1 or 3 arguments; received %i." % len(args)) - return margins[0], margins[1], margins[3], margins[4] + return margin[0], margin[1], margin[3], margin[4] - def dcgain(*args): ''' Compute the gain of the system in steady state. Modified: trunk/src/statesp.py =================================================================== --- trunk/src/statesp.py 2011-07-13 16:03:32 UTC (rev 167) +++ trunk/src/statesp.py 2011-07-16 06:05:46 UTC (rev 168) @@ -208,7 +208,7 @@ return StateSpace(self.A, self.B, -self.C, -self.D) - # Addition of two transfer functions (parallel interconnection) + # Addition of two state space systems (parallel interconnection) def __add__(self, other): """Add two LTI systems (parallel connection).""" @@ -244,7 +244,7 @@ return self + other - # Subtraction of two transfer functions (parallel interconnection) + # Subtraction of two state space systems (parallel interconnection) def __sub__(self, other): """Subtract two LTI systems.""" @@ -255,7 +255,7 @@ return other + (-self) - # Multiplication of two transfer functions (series interconnection) + # Multiplication of two state space systems (series interconnection) def __mul__(self, other): """Multiply two LTI objects (serial connection).""" @@ -284,7 +284,7 @@ return StateSpace(A, B, C, D) - # Right multiplication of two transfer functions (series interconnection) + # Right multiplication of two state space systems (series interconnection) # Just need to convert LH argument to a state space object def __rmul__(self, other): """Right multiply two LTI objects (serial connection).""" Modified: trunk/tests/matlab_test.py =================================================================== --- trunk/tests/matlab_test.py 2011-07-13 16:03:32 UTC (rev 167) +++ trunk/tests/matlab_test.py 2011-07-16 06:05:46 UTC (rev 168) @@ -186,6 +186,13 @@ [-0.1391, 48.9776]]) yout, _t, _xout = lsim(self.mimo_ss1, u, t, x0) np.testing.assert_array_almost_equal(yout, youttrue, decimal=4) + + def testMargin(self): + #! TODO: check results to make sure they are OK + gm, pm, wg, wp = margin(self.siso_tf1); + gm, pm, wg, wp = margin(self.siso_tf2); + gm, pm, wg, wp = margin(self.siso_ss1); + gm, pm, wg, wp = margin(self.siso_ss2); def testDcgain(self): #Create different forms of a SISO system This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2011-07-26 05:44:07
|
Revision: 171 http://python-control.svn.sourceforge.net/python-control/?rev=171&view=rev Author: murrayrm Date: 2011-07-26 05:44:01 +0000 (Tue, 26 Jul 2011) Log Message: ----------- updated test suite for phaseplot Modified Paths: -------------- trunk/ChangeLog trunk/tests/phaseplot_test.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-07-26 02:41:03 UTC (rev 170) +++ trunk/ChangeLog 2011-07-26 05:44:01 UTC (rev 171) @@ -1,5 +1,8 @@ 2011-07-25 Richard Murray <murray@malabar.local> + * tests/phaseplot_test.py: updated unit tests to use new call + signatures + * examples/phaseplots.py: updated calls to PhasePlot to use new argument structure Modified: trunk/tests/phaseplot_test.py =================================================================== --- trunk/tests/phaseplot_test.py 2011-07-26 02:41:03 UTC (rev 170) +++ trunk/tests/phaseplot_test.py 2011-07-26 05:44:01 UTC (rev 171) @@ -25,32 +25,33 @@ def testInvPendSims(self): PhasePlot(self.invpend_ode, (-6,6,10), (-6,6,10), - xinit = ([1,1], [-1,1])); + X0 = ([1,1], [-1,1])); def testInvPendTimePoints(self): PhasePlot(self.invpend_ode, (-6,6,10), (-6,6,10), - xinit = ([1,1], [-1,1]), T=np.linspace(0,5,100)); + X0 = ([1,1], [-1,1]), T=np.linspace(0,5,100)); def testInvPendLogtime(self): - PhasePlot(self.invpend_ode, - 'logtime', (3, 0.7), None, + PhasePlot(self.invpend_ode, X0 = [ [-2*pi, 1.6], [-2*pi, 0.5], [-1.8, 2.1], [-1, 2.1], [4.2, 2.1], [5, 2.1], [2*pi, -1.6], [2*pi, -0.5], [1.8, -2.1], [1, -2.1], [-4.2, -2.1], [-5, -2.1] ], - np.linspace(0, 40, 200), verbose=False) + T = np.linspace(0, 40, 200), + logtime=(3, 0.7), + verbose=False) def testInvPendAuto(self): - PhasePlot(self.invpend_ode, 'auto', 0, None, - [[-2.3056, 2.1], [2.3056, -2.1]], 6, verbose=False) + PhasePlot(self.invpend_ode, lingrid = 0, X0= + [[-2.3056, 2.1], [2.3056, -2.1]], T=6, verbose=False) def testOscillatorParams(self): m = 1; b = 1; k = 1; # default values - PhasePlot(self.oscillator_ode, 'timepts', [0.3, 1, 2, 3], None, + PhasePlot(self.oscillator_ode, timepts = [0.3, 1, 2, 3], X0 = [[-1,1], [-0.3,1], [0,1], [0.25,1], [0.5,1], [0.7,1], [1,1], [1.3,1], [1,-1], [0.3,-1], [0,-1], [-0.25,-1], [-0.5,-1], [-0.7,-1], [-1,-1], [-1.3,-1]], - np.linspace(0, 10, 100), parms = (m, b, k)); + T = np.linspace(0, 10, 100), parms = (m, b, k)); # Sample dynamical systems - inverted pendulum def invpend_ode(self, x, t, m=1., l=1., b=0, g=9.8): This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2012-01-08 03:33:48
|
Revision: 181 http://python-control.svn.sourceforge.net/python-control/?rev=181&view=rev Author: murrayrm Date: 2012-01-08 03:33:41 +0000 (Sun, 08 Jan 2012) Log Message: ----------- * Restructured documentation (not completed) * Added in code from Ryan Krauss for operation when slycot is missing * Updated version number to 0.5b * See ChangeLog for a more detailed list of changes Modified Paths: -------------- trunk/ChangeLog trunk/doc/bdalg_strings.rst trunk/doc/class_strings.rst trunk/doc/conf.py trunk/doc/index.rst trunk/doc/intro.rst trunk/doc/matlab_strings.rst trunk/doc/modsimp_strings.rst trunk/examples/rss-balred.py trunk/setup.py trunk/src/statesp.py trunk/src/xferfcn.py trunk/tests/slycot_convert_test.py Added Paths: ----------- trunk/doc/analysis.rst trunk/doc/examples.rst trunk/doc/freqplot.rst trunk/doc/modules.rst trunk/doc/synthesis.rst trunk/doc/timeresp.rst Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/ChangeLog 2012-01-08 03:33:41 UTC (rev 181) @@ -1,5 +1,59 @@ +2012-01-07 Richard Murray <murray@malabar.local> + + * doc/modules.rst: added new sections for analysis, synthesis, + frequency plots and time response. + + * doc/index.rst (Contents): added modules and examples. + + * src/xferfcn.py (_convertToTransferFunction): added check for + slycot import error. If not present, use signal.lti to perform the + conversion. Only works for SISO. + + * src/statesp.py (_convertToStateSpace): added check for slycot + import error, to allow basic functionality without the presence of + slycot (contributed by Ryan Krauss). + + * tests/slycot_convert_test.py (TestSlycot.testTF): moved slycot + import into test function, so that test script still works even if + time response is not present. + +2011-08-09 Richard Murray <murray@malabar.local> + + * src/timeresp.py: fixed doc reference to time-series-convention + +2011-08-08 Richard Murray <murray@malabar.local> + + * doc/index.rst, doc/modules.rst: Moved MATLAB section into + python-control modules file + 2011-08-07 Richard Murray <murray@malabar.local> + * doc/conf.py: added autosummary extension + + * doc/timeresp.rst: New file containing listing of functions for + time responses (step, initial, etc) + + * doc/freqplot.rst: New file listing functions for frequency + plots (bode, nyquist, etc) + + * doc/modules.rst: New file that contains a listing of the major + modules in the toolbox + + * doc/index.rst (Contents): Rearranged contents to put modules + documentation in a separate section. Also added examples section + (not quite ready yet). + +2011-08-07 Richard Murray <murray@malabar.local> + + * examples/rss-balred.py: reordered outputs from call to matlab.step + (missed this the first time around) + + * doc/matlab_strings.rst: Matlab -> MATLAB (proper spelling) + + * setup.py, doc/conf.py: updated version to 0.5b + +2011-08-07 Richard Murray <murray@malabar.local> + * doc/conf.py: Updated version numbers to 0.5a, regenerated documentation and reposted on web (note that 0.5a tag and source distribution have this wrong) Added: trunk/doc/analysis.rst =================================================================== --- trunk/doc/analysis.rst (rev 0) +++ trunk/doc/analysis.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -0,0 +1,2 @@ +Control System Analysis +*********************** \ No newline at end of file Modified: trunk/doc/bdalg_strings.rst =================================================================== --- trunk/doc/bdalg_strings.rst 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/doc/bdalg_strings.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -1,7 +1,7 @@ -Block Diagram Algebra Routines -****************************** +Block Diagram Algebra +********************* -The Block Diagram Algebra Module -================================ +.. toctree:: + .. automodule:: bdalg :members: Modified: trunk/doc/class_strings.rst =================================================================== --- trunk/doc/class_strings.rst 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/doc/class_strings.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -1,12 +1,12 @@ Python-Control Classes ********************** -The State Space Module -====================== +State Space Class +================= .. automodule:: statesp :members: -The Transfer Function Module -============================ +Transfer Function Class +======================= .. automodule:: xferfcn - :members: \ No newline at end of file + :members: Modified: trunk/doc/conf.py =================================================================== --- trunk/doc/conf.py 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/doc/conf.py 2012-01-08 03:33:41 UTC (rev 181) @@ -34,7 +34,8 @@ # ``sphinx.ext.viewcode`` : Include highlighted source code in the # documentation extensions = ['sphinx.ext.autodoc', 'numpydoc', 'sphinx.ext.pngmath', - 'sphinx.ext.intersphinx', 'sphinx.ext.todo'] + 'sphinx.ext.intersphinx', 'sphinx.ext.todo', + 'sphinx.ext.autosummary'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -49,7 +50,7 @@ master_doc = 'index' # General information about the project. -project = u'Python Control' +project = u'Python Control Systems Library' copyright = u'2011, Richard M. Murray et al.' # The version info for the project you're documenting, acts as replacement for @@ -57,9 +58,9 @@ # built documents. # # The short X.Y version. -version = '0.5a' +version = '0.5b' # The full version, including alpha/beta/rc tags. -release = '0.5a' +release = '0.5b' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -99,7 +100,7 @@ #should be linked to in this documentation. intersphinx_mapping = \ {'scipy':('http://docs.scipy.org/doc/scipy/reference/', None), - 'numpy':('http://docs.scipy.org/doc/numpy/reference/', None)} + 'numpy':('http://docs.scipy.org/doc/numpy', None)} #If this is True, todo and todolist produce output, else they produce nothing. #The default is False. Added: trunk/doc/examples.rst =================================================================== --- trunk/doc/examples.rst (rev 0) +++ trunk/doc/examples.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -0,0 +1,2 @@ +Examples +******** Added: trunk/doc/freqplot.rst =================================================================== --- trunk/doc/freqplot.rst (rev 0) +++ trunk/doc/freqplot.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -0,0 +1,15 @@ +Frequency Domain Plotting +************************* + +.. toctree:: + +Plotting routines +================= +.. autofunction:: freqplot.bode_plot +.. autofunction:: freqplot.nyquist_plot +.. autofunction:: freqplot.gangof4_plot +.. autofunction:: nichols.nichols_plot + +Utility functions +================= +.. autofunction:: freqplot.default_frequency_range Modified: trunk/doc/index.rst =================================================================== --- trunk/doc/index.rst 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/doc/index.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -3,18 +3,23 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to Python-Control's documentation! -========================================== +Python Control User's Manual +============================ +Welcome to the Python Control Systems Library (python-control) User's +Manual. This manual describes the python-control package, including +all of the functions defined in the package and examples showing how +to use the package. + Contents: .. toctree:: + :maxdepth: 2 intro class_strings - modsimp_strings - matlab_strings - bdalg_strings + modules + examples Indices and tables ================== Modified: trunk/doc/intro.rst =================================================================== --- trunk/doc/intro.rst 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/doc/intro.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -2,8 +2,14 @@ Introduction ============ -Welcome to the Python-Control project. +Welcome to the Python Control Systems Toolbox (python-control) User's +Manual. This manual contains information on using the python-control +package, including documentation for all functions in the package and +examples illustrating their use. +Overview of the Toolbox +----------------------- + The python-control package is a set of python classes and functions that implement common operations for the analysis and design of feedback control systems. The initial goal is to implement all of the @@ -39,15 +45,22 @@ Getting Started --------------- 1. Download latest release from http://sf.net/projects/python-control/files. + 2. Untar the source code in a temporary directory and run 'python setup.py install' to build and install the code -3. To see if things are working correctly, run ipython -pylab and run the - script 'examples/secord-matlab.py'. This should generate a set response, - Bode plot and Nyquist plot for a simple second order system. + +3. To see if things are working correctly, run ipython -pylab and run + the script 'examples/secord-matlab.py'. This should generate a + step response, Bode plot and Nyquist plot for a simple second order + system. + 4. To see the commands that are available, run the following commands in ipython:: + >>> import control >>> ?control.matlab + 5. If you want to have a MATLAB-like environment for running the control toolbox, use:: + >>> from control.matlab import * Modified: trunk/doc/matlab_strings.rst =================================================================== --- trunk/doc/matlab_strings.rst 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/doc/matlab_strings.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -1,8 +1,6 @@ -Matlab-like Routines -******************** +MATLAB Compatibility Module +*************************** -The Matlab Module -================= .. automodule:: matlab :members: Modified: trunk/doc/modsimp_strings.rst =================================================================== --- trunk/doc/modsimp_strings.rst 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/doc/modsimp_strings.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -1,7 +1,5 @@ Model Simplification Tools ************************** -The modelsimp Module -==================== .. automodule:: modelsimp :members: Added: trunk/doc/modules.rst =================================================================== --- trunk/doc/modules.rst (rev 0) +++ trunk/doc/modules.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -0,0 +1,12 @@ +Python-Control Modules +********************** + +.. toctree:: + + bdalg_strings + analysis + freqplot + timeresp + synthesis + modsimp_strings + matlab_strings Added: trunk/doc/synthesis.rst =================================================================== --- trunk/doc/synthesis.rst (rev 0) +++ trunk/doc/synthesis.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -0,0 +1,2 @@ +Control System Synthesis +************************ \ No newline at end of file Added: trunk/doc/timeresp.rst =================================================================== --- trunk/doc/timeresp.rst (rev 0) +++ trunk/doc/timeresp.rst 2012-01-08 03:33:41 UTC (rev 181) @@ -0,0 +1,8 @@ +Time Domain Simulation +********************** + +.. automodule:: timeresp + :members: + +.. autofunction:: phaseplot.phase_plot +.. autofunction:: phaseplot.box_grid Modified: trunk/examples/rss-balred.py =================================================================== --- trunk/examples/rss-balred.py 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/examples/rss-balred.py 2012-01-08 03:33:41 UTC (rev 181) @@ -26,8 +26,8 @@ # Comparison of the step responses of the full and reduced systems plt.figure(1) -t, y = mt.step(fsys) -tr, yr = mt.step(rsys) +y, t = mt.step(fsys) +yr, tr = mt.step(rsys) plt.plot(t.T, y.T) plt.hold(True) plt.plot(tr.T, yr.T) Modified: trunk/setup.py =================================================================== --- trunk/setup.py 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/setup.py 2012-01-08 03:33:41 UTC (rev 181) @@ -4,7 +4,7 @@ from setuptools import setup setup(name = 'control', - version = '0.5a', + version = '0.5b', description = 'Python Control Systems Library', author = 'Richard Murray', author_email = 'mu...@cd...', Modified: trunk/src/statesp.py =================================================================== --- trunk/src/statesp.py 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/src/statesp.py 2012-01-08 03:33:41 UTC (rev 181) @@ -75,7 +75,7 @@ from numpy import all, angle, any, array, asarray, concatenate, cos, delete, \ dot, empty, exp, eye, matrix, ones, pi, poly, poly1d, roots, shape, sin, \ - zeros + zeros, squeeze from numpy.random import rand, randn from numpy.linalg import inv, det, solve from numpy.linalg.linalg import LinAlgError @@ -459,28 +459,37 @@ # Already a state space system; just return it return sys elif isinstance(sys, xferfcn.TransferFunction): - from slycot import td04ad - if len(kw): - raise TypeError("If sys is a TransferFunction, _convertToStateSpace \ -cannot take keywords.") + try: + from slycot import td04ad + if len(kw): + raise TypeError("If sys is a TransferFunction, _convertToStateSpace \ + cannot take keywords.") - # Change the numerator and denominator arrays so that the transfer - # function matrix has a common denominator. - num, den = sys._common_den() - # Make a list of the orders of the denominator polynomials. - index = [len(den) - 1 for i in range(sys.outputs)] - # Repeat the common denominator along the rows. - den = array([den for i in range(sys.outputs)]) - # TODO: transfer function to state space conversion is still buggy! - #print num - #print shape(num) - ssout = td04ad('R',sys.inputs, sys.outputs, index, den, num,tol=0.0) - - states = ssout[0] - return StateSpace(ssout[1][:states, :states], - ssout[2][:states, :sys.inputs], - ssout[3][:sys.outputs, :states], - ssout[4]) + # Change the numerator and denominator arrays so that the transfer + # function matrix has a common denominator. + num, den = sys._common_den() + # Make a list of the orders of the denominator polynomials. + index = [len(den) - 1 for i in range(sys.outputs)] + # Repeat the common denominator along the rows. + den = array([den for i in range(sys.outputs)]) + # TODO: transfer function to state space conversion is still buggy! + #print num + #print shape(num) + ssout = td04ad('R',sys.inputs, sys.outputs, index, den, num,tol=0.0) + + states = ssout[0] + return StateSpace(ssout[1][:states, :states], + ssout[2][:states, :sys.inputs], + ssout[3][:sys.outputs, :states], + ssout[4]) + except ImportError: + lti_sys = lti(squeeze(sys.num), squeeze(sys.den))#<-- do we want to squeeze first + # and check dimenations? I think + # this will fail if num and den aren't 1-D + # after the squeeze + return StateSpace(lti_sys.A, lti_sys.B, lti_sys.C, lti_sys.D) + + elif isinstance(sys, (int, long, float, complex)): if "inputs" in kw: inputs = kw["inputs"] @@ -627,8 +636,8 @@ If ``sys`` is already a SISO system, it will be returned unaltered. - Parameters: - + Parameters + ---------- sys: StateSpace Linear (MIMO) system that should be converted. input: int Modified: trunk/src/xferfcn.py =================================================================== --- trunk/src/xferfcn.py 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/src/xferfcn.py 2012-01-08 03:33:41 UTC (rev 181) @@ -76,7 +76,7 @@ # External function declarations from numpy import angle, any, array, empty, finfo, insert, ndarray, ones, \ - polyadd, polymul, polyval, roots, sort, sqrt, zeros + polyadd, polymul, polyval, roots, sort, sqrt, zeros, squeeze from scipy.signal import lti from copy import deepcopy from lti import Lti @@ -767,27 +767,39 @@ return sys elif isinstance(sys, statesp.StateSpace): - from slycot import tb04ad - if len(kw): - raise TypeError("If sys is a StateSpace, _convertToTransferFunction \ -cannot take keywords.") + try: + from slycot import tb04ad + if len(kw): + raise TypeError("If sys is a StateSpace, \ + _convertToTransferFunction cannot take keywords.") - # Use Slycot to make the transformation - # Make sure to convert system matrices to numpy arrays - tfout = tb04ad(sys.states, sys.inputs, sys.outputs, array(sys.A), - array(sys.B), array(sys.C), array(sys.D), tol1=0.0) + # Use Slycot to make the transformation + # Make sure to convert system matrices to numpy arrays + tfout = tb04ad(sys.states, sys.inputs, sys.outputs, array(sys.A), + array(sys.B), array(sys.C), array(sys.D), tol1=0.0) - # Preallocate outputs. - num = [[[] for j in range(sys.inputs)] for i in range(sys.outputs)] - den = [[[] for j in range(sys.inputs)] for i in range(sys.outputs)] + # Preallocate outputs. + num = [[[] for j in range(sys.inputs)] for i in range(sys.outputs)] + den = [[[] for j in range(sys.inputs)] for i in range(sys.outputs)] - for i in range(sys.outputs): - for j in range(sys.inputs): - num[i][j] = list(tfout[6][i, j, :]) - # Each transfer function matrix row has a common denominator. - den[i][j] = list(tfout[5][i, :]) - # print num - # print den + for i in range(sys.outputs): + for j in range(sys.inputs): + num[i][j] = list(tfout[6][i, j, :]) + # Each transfer function matrix row has a common denominator. + den[i][j] = list(tfout[5][i, :]) + # print num + # print den + except ImportError: + # If slycot is not available, use signal.lti (SISO only) + if (sys.inputs != 1 or sys.outputs != 1): + raise TypeError("No support for MIMO without slycot") + + lti_sys = lti(sys.A, sys.B, sys.C, sys.D) + num = squeeze(lti_sys.num) + den = squeeze(lti_sys.den) + print num + print den + return TransferFunction(num, den) elif isinstance(sys, (int, long, float, complex)): if "inputs" in kw: Modified: trunk/tests/slycot_convert_test.py =================================================================== --- trunk/tests/slycot_convert_test.py 2012-01-08 02:57:46 UTC (rev 180) +++ trunk/tests/slycot_convert_test.py 2012-01-08 03:33:41 UTC (rev 181) @@ -5,7 +5,6 @@ import unittest import numpy as np -from slycot import tb04ad, td04ad import control.matlab as matlab class TestSlycot(unittest.TestCase): @@ -30,6 +29,7 @@ """ Directly tests the functions tb04ad and td04ad through direct comparison of transfer function coefficients. Similar to convert_test, but tests at a lower level. """ + from slycot import tb04ad, td04ad for states in range(1, self.maxStates): for inputs in range(1, self.maxI+1): for outputs in range(1, self.maxO+1): This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2012-08-25 17:46:02
|
Revision: 184 http://python-control.svn.sourceforge.net/python-control/?rev=184&view=rev Author: murrayrm Date: 2012-08-25 17:45:56 +0000 (Sat, 25 Aug 2012) Log Message: ----------- commiting missing unit test with fix for ordering differences Modified Paths: -------------- trunk/ChangeLog Added Paths: ----------- trunk/tests/rlocus_test.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2012-08-25 16:11:20 UTC (rev 183) +++ trunk/ChangeLog 2012-08-25 17:45:56 UTC (rev 184) @@ -1,3 +1,27 @@ +2012-08-25 Richard Murray <murray@altura.local> + + * tests/rlocus_test.py (TestRootLocus.testRootLocus): added sort() + to test to get rid of problems in which ordering was generating an + error. + + * src/freqplot.py (nyquist_plot): added code from Kevin Davies to + label frequency points in Nyquist plot + + * src/__init__.py: import non-conflicting MATLAB functions by default + + * src/freqplot.py (bode_plot, nyquist_plot): simplified code and + removed unneeded options. To set color, linestyle, etc use keywords + and pass to matplotlib. + +2012-08-24 Richard Murray <murray@altura.local> + + * src/freqplot.py: added in plot enhancements from Kevin Davies + (bode_plot): pass optional arguments and keywords to matplotlib + (get_pow1000): new function for determinine engineering exponent + (gen_prefix): new function to get SI prefix for power of 1000 + + * src/freqplot.py (bode_plot): removed extraneous phase_deg calculation + 2012-01-07 Richard Murray <murray@malabar.local> * doc/modules.rst: added new sections for analysis, synthesis, Added: trunk/tests/rlocus_test.py =================================================================== --- trunk/tests/rlocus_test.py (rev 0) +++ trunk/tests/rlocus_test.py 2012-08-25 17:45:56 UTC (rev 184) @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# +# rlocus_test.py - unit test for root locus diagrams +# RMM, 1 Jul 2011 + +import unittest +import numpy as np +from control.rlocus import root_locus +from control.xferfcn import TransferFunction +from control.statesp import StateSpace +from control.bdalg import feedback + +class TestRootLocus(unittest.TestCase): + """These are tests for the feedback function in rlocus.py.""" + + def setUp(self): + """This contains some random LTI systems and scalars for testing.""" + + # Two random SISO systems. + self.sys1 = TransferFunction([1, 2], [1, 2, 3]) + self.sys2 = StateSpace([[1., 4.], [3., 2.]], [[1.], [-4.]], + [[1., 0.]], [[0.]]) + + def testRootLocus(self): + """Basic root locus plot""" + klist = [-1, 0, 1]; + rlist = root_locus(self.sys1, [-1, 0, 1], Plot=False) + + for k in klist: + np.testing.assert_array_almost_equal( + np.sort(rlist[k]), + np.sort(feedback(self.sys1, klist[k]).pole())) + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestRootLocus) + +if __name__ == "__main__": + unittest.main() This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2012-08-30 05:44:39
|
Revision: 185 http://python-control.svn.sourceforge.net/python-control/?rev=185&view=rev Author: murrayrm Date: 2012-08-30 05:44:32 +0000 (Thu, 30 Aug 2012) Log Message: ----------- * Fixed bug in statespace -> transfer function conversation in the way that complex conjugate pairs of poles with multiplicity > 1 were handled * Fixed bugs in tests/convert_test in the way that state space and transfer function conversions were tested; added tf to ss comparison * Replace nd.size() with len() in place() to fix bug reported by R. Bucher * See ChangeLog for more detailed descriptions Modified Paths: -------------- trunk/ChangeLog trunk/src/__init__.py trunk/src/matlab.py trunk/src/statefbk.py trunk/src/statesp.py trunk/src/xferfcn.py trunk/tests/convert_test.py trunk/tests/matlab_test.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2012-08-25 17:45:56 UTC (rev 184) +++ trunk/ChangeLog 2012-08-30 05:44:32 UTC (rev 185) @@ -1,5 +1,36 @@ +2012-08-29 Richard Murray <murray@altura.local> + + * src/xferfcn.py (TransferFunction._common_den): fixed up code for + case where poles have multiplicity > 1. Set end limits of check to + avoid overruning the list + combine complex conjugate pole pairs + from the outside in to prevent small errors from growing. + +2012-08-26 Richard Murray <murray@altura.local> + + * tests/convert_test.py (TestConvert.testConvert): replaced previous + test of transfer function coefficients with a frequency response + test. This is necessary because various degenerate conditions will + generate a situation where the transfer functions are of different + forms but still equal. Eg 0/1 = 1e-17 s + 1e-16 / (s^2 + s + 1). + Still getting errors, but looks like actual problem in conversion. + + * src/statesp.py (_mimo2siso): set default value for warn_conversion + to False + + * tests/convert_test.py (TestConvert.testConvert): Added check to + make sure that we don't create problems with uncontrollable or + unobservable systems. + 2012-08-25 Richard Murray <murray@altura.local> + * src/xferfcn.py (TransferFunction._common_den): identified bug in + the way that common denominators are computed; see comments in code + regarding complex conjugate pairs. + + * src/statefbk.py (place): repalced nd.size(placed_eigs) with + len(placed_eigs) to fix intermittent problems pointed out by Roberto + Bucher in control-0.3c. + * tests/rlocus_test.py (TestRootLocus.testRootLocus): added sort() to test to get rid of problems in which ordering was generating an error. Modified: trunk/src/__init__.py =================================================================== --- trunk/src/__init__.py 2012-08-25 17:45:56 UTC (rev 184) +++ trunk/src/__init__.py 2012-08-30 05:44:32 UTC (rev 185) @@ -74,9 +74,9 @@ from xferfcn import TransferFunction # Import some of the more common (and benign) MATLAB shortcuts +# By default, don't import conflicting commands here from matlab import ss, tf, ss2tf, tf2ss, drss from matlab import pole, zero, evalfr, freqresp, dcgain from matlab import nichols, rlocus, margin # bode and nyquist come directly from freqplot.py from matlab import step, impulse, initial, lsim - Modified: trunk/src/matlab.py =================================================================== --- trunk/src/matlab.py 2012-08-25 17:45:56 UTC (rev 184) +++ trunk/src/matlab.py 2012-08-30 05:44:32 UTC (rev 185) @@ -984,6 +984,7 @@ # Warn about unimplemented plotstyles #! TODO: remove this when plot styles are implemented in bode() + #! TODO: uncomment unit test code that tests this out if (len(plotstyle) != 0): print("Warning (matabl.bode): plot styles not implemented"); Modified: trunk/src/statefbk.py =================================================================== --- trunk/src/statefbk.py 2012-08-25 17:45:56 UTC (rev 184) +++ trunk/src/statefbk.py 2012-08-30 05:44:32 UTC (rev 185) @@ -93,7 +93,7 @@ # Call SLICOT routine to place the eigenvalues A_z,w,nfp,nap,nup,F,Z = \ - sb01bd(B_mat.shape[0], B_mat.shape[1], np.size(placed_eigs), alpha, + sb01bd(B_mat.shape[0], B_mat.shape[1], len(placed_eigs), alpha, A_mat, B_mat, placed_eigs, 'C'); # Return the gain matrix, with MATLAB gain convention Modified: trunk/src/statesp.py =================================================================== --- trunk/src/statesp.py 2012-08-25 17:45:56 UTC (rev 184) +++ trunk/src/statesp.py 2012-08-30 05:44:32 UTC (rev 185) @@ -472,7 +472,7 @@ index = [len(den) - 1 for i in range(sys.outputs)] # Repeat the common denominator along the rows. den = array([den for i in range(sys.outputs)]) - # TODO: transfer function to state space conversion is still buggy! + #! TODO: transfer function to state space conversion is still buggy! #print num #print shape(num) ssout = td04ad('R',sys.inputs, sys.outputs, index, den, num,tol=0.0) @@ -624,7 +624,7 @@ return StateSpace(A, B, C, D) # Convert a MIMO system to a SISO system -def _mimo2siso(sys, input, output, warn_conversion): +def _mimo2siso(sys, input, output, warn_conversion=False): #pylint: disable=W0622 """ Convert a MIMO system to a SISO system. (Convert a system with multiple Modified: trunk/src/xferfcn.py =================================================================== --- trunk/src/xferfcn.py 2012-08-25 17:45:56 UTC (rev 184) +++ trunk/src/xferfcn.py 2012-08-30 05:44:32 UTC (rev 185) @@ -638,14 +638,34 @@ if abs(poles[n].imag) > 10 * eps: # To prevent buildup of imaginary part error, handle complex # pole pairs together. - quad = polymul([1., -poles[n]], [1., -poles[n+1]]) - assert all(quad.imag < 10 * eps), \ - "The quadratic has a nontrivial imaginary part: %g" \ - % quad.imag.max() - quad = quad.real + # + # Because we might have repeated real parts of poles + # and the fact that we are using lexigraphical + # ordering, we can't just combine adjacent poles. + # Instead, we have to figure out the multiplicity + # first, then multiple the pairs from the outside in. - den = polymul(den, quad) - n += 2 + # Figure out the multiplicity + m = 1; # multiplicity count + while (n+m < len(poles) and + poles[n].real == poles[n+m].real and + poles[n].imag * poles[n+m].imag > 0): + m += 1 + + if (m > 1): + print "Found pole with multiplicity %d" % m + # print "Poles = ", poles + + # Multiple pairs from the outside in + for i in range(m): + quad = polymul([1., -poles[n]], [1., -poles[n+2*(m-i)-1]]) + assert all(quad.imag < 10 * eps), \ + "Quadratic has a nontrivial imaginary part: %g" \ + % quad.imag.max() + + den = polymul(den, quad.real) + n += 1 # move to next pair + n += m # skip past conjugate pairs else: den = polymul(den, [1., -poles[n].real]) n += 1 @@ -654,6 +674,7 @@ num = deepcopy(self.num) if isinstance(den,float): den = array([den]) + for i in range(self.outputs): for j in range(self.inputs): # The common denominator has leading coefficient 1. Scale out @@ -661,9 +682,11 @@ assert self.den[i][j][0], "The i = %i, j = %i denominator has \ a zero leading coefficient." % (i, j) num[i][j] = num[i][j] / self.den[i][j][0] + # Multiply in the missing poles. for p in missingpoles[i][j]: num[i][j] = polymul(num[i][j], [1., -p]) + # Pad all numerator polynomials with zeros so that the numerator arrays # are the same size as the denominator. for i in range(self.outputs): @@ -672,11 +695,12 @@ if(pad>0): num[i][j] = insert(num[i][j], zeros(pad), zeros(pad)) + # Finally, convert the numerator to a 3-D array. num = array(num) # Remove trivial imaginary parts. Check for nontrivial imaginary parts. if any(abs(num.imag) > sqrt(eps)): - print ("Warning: The numerator has a nontrivial nontrivial part: %g" + print ("Warning: The numerator has a nontrivial imaginary part: %g" % abs(num.imag).max()) num = num.real Modified: trunk/tests/convert_test.py =================================================================== --- trunk/tests/convert_test.py 2012-08-25 17:45:56 UTC (rev 184) +++ trunk/tests/convert_test.py 2012-08-30 05:44:32 UTC (rev 185) @@ -16,6 +16,7 @@ import unittest import numpy as np +import control import control.matlab as matlab class TestConvert(unittest.TestCase): @@ -27,9 +28,9 @@ # Number of times to run each of the randomized tests. self.numTests = 1 #almost guarantees failure # Maximum number of states to test + 1 - self.maxStates = 20 + self.maxStates = 4 # Maximum number of inputs and outputs to test + 1 - self.maxIO = 10 + self.maxIO = 5 # Set to True to print systems to the output. self.debug = False @@ -42,20 +43,35 @@ def testConvert(self): """Test state space to transfer function conversion.""" - #Currently it only tests that a TF->SS->TF generates an unchanged TF verbose = self.debug + from control.statesp import _mimo2siso #print __doc__ + # Machine precision for floats. + eps = np.finfo(float).eps + for states in range(1, self.maxStates): for inputs in range(1, self.maxIO): for outputs in range(1, self.maxIO): - #start with a random SS system and transform to TF - #then back to SS, check that the matrices are the same. + # start with a random SS system and transform to TF then + # back to SS, check that the matrices are the same. ssOriginal = matlab.rss(states, inputs, outputs) if (verbose): self.printSys(ssOriginal, 1) + # Make sure the system is not degenerate + Cmat = control.ctrb(ssOriginal.A, ssOriginal.B) + if (np.linalg.matrix_rank(Cmat) != states): + if (verbose): + print " skipping (not reachable)" + continue + Omat = control.obsv(ssOriginal.A, ssOriginal.C) + if (np.linalg.matrix_rank(Omat) != states): + if (verbose): + print " skipping (not observable)" + continue + tfOriginal = matlab.tf(ssOriginal) if (verbose): self.printSys(tfOriginal, 2) @@ -67,27 +83,77 @@ tfTransformed = matlab.tf(ssTransformed) if (verbose): self.printSys(tfTransformed, 4) - + + # Check to see if the state space systems have same dim + if (ssOriginal.states != ssTransformed.states): + print "WARNING: state space dimension mismatch: " + \ + "%d versus %d" % \ + (ssOriginal.states, ssTransformed.states) + + # Now make sure the frequency responses match + # Since bode() only handles SISO, go through each I/O pair + # For phase, take sine and cosine to avoid +/- 360 offset for inputNum in range(inputs): for outputNum in range(outputs): - np.testing.assert_array_almost_equal(\ - tfOriginal.num[outputNum][inputNum], \ - tfTransformed.num[outputNum][inputNum], \ - err_msg='numerator mismatch') + if (verbose): + print "Checking input %d, output %d" \ + % (inputNum, outputNum) + ssorig_mag, ssorig_phase, ssorig_omega = \ + control.bode(_mimo2siso(ssOriginal, \ + inputNum, outputNum), \ + deg=False, Plot=False) + ssorig_real = ssorig_mag * np.cos(ssorig_phase) + ssorig_imag = ssorig_mag * np.sin(ssorig_phase) + + # + # Make sure TF has same frequency response + # + num = tfOriginal.num[outputNum][inputNum] + den = tfOriginal.den[outputNum][inputNum] + tforig = control.tf(num, den) + + tforig_mag, tforig_phase, tforig_omega = \ + control.bode(tforig, ssorig_omega, \ + deg=False, Plot=False) + + tforig_real = tforig_mag * np.cos(tforig_phase) + tforig_imag = tforig_mag * np.sin(tforig_phase) + np.testing.assert_array_almost_equal( \ + ssorig_real, tforig_real) + np.testing.assert_array_almost_equal( \ + ssorig_imag, tforig_imag) + + # + # Make sure xform'd SS has same frequency response + # + ssxfrm_mag, ssxfrm_phase, ssxfrm_omega = \ + control.bode(_mimo2siso(ssTransformed, \ + inputNum, outputNum), \ + ssorig_omega, \ + deg=False, Plot=False) + ssxfrm_real = ssxfrm_mag * np.cos(ssxfrm_phase) + ssxfrm_imag = ssxfrm_mag * np.sin(ssxfrm_phase) + np.testing.assert_array_almost_equal( \ + ssorig_real, ssxfrm_real) + np.testing.assert_array_almost_equal( \ + ssorig_imag, ssxfrm_imag) + + # + # Make sure xform'd TF has same frequency response + # + num = tfTransformed.num[outputNum][inputNum] + den = tfTransformed.den[outputNum][inputNum] + tfxfrm = control.tf(num, den) + tfxfrm_mag, tfxfrm_phase, tfxfrm_omega = \ + control.bode(tfxfrm, ssorig_omega, \ + deg=False, Plot=False) - np.testing.assert_array_almost_equal(\ - tfOriginal.den[outputNum][inputNum], \ - tfTransformed.den[outputNum][inputNum], - err_msg='denominator mismatch') - - #To test the ss systems is harder because they aren't the same - #realization. This could be done with checking that they have the - #same freq response with bode, but apparently it doesn't work - #the way it should right now: - ## Bode should work like this: - #[mag,phase,freq]=bode(sys) - #it doesn't seem to...... - #This should be added. + tfxfrm_real = tfxfrm_mag * np.cos(tfxfrm_phase) + tfxfrm_imag = tfxfrm_mag * np.sin(tfxfrm_phase) + np.testing.assert_array_almost_equal( \ + ssorig_real, tfxfrm_real) + np.testing.assert_array_almost_equal( \ + ssorig_imag, tfxfrm_imag) def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestConvert) Modified: trunk/tests/matlab_test.py =================================================================== --- trunk/tests/matlab_test.py 2012-08-25 17:45:56 UTC (rev 184) +++ trunk/tests/matlab_test.py 2012-08-30 05:44:32 UTC (rev 185) @@ -238,7 +238,8 @@ w = logspace(-3, 3); bode(self.siso_ss1, w) bode(self.siso_ss1, self.siso_tf2, w) - bode(self.siso_ss1, '-', self.siso_tf1, 'b--', self.siso_tf2, 'k.') +# Not yet implemented +# bode(self.siso_ss1, '-', self.siso_tf1, 'b--', self.siso_tf2, 'k.') def testRlocus(self): rlocus(self.siso_ss1) This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2010-11-06 13:04:03
|
Revision: 29 http://python-control.svn.sourceforge.net/python-control/?rev=29&view=rev Author: murrayrm Date: 2010-11-06 13:03:55 +0000 (Sat, 06 Nov 2010) Log Message: ----------- 2010-11-05 Richard Murray <murray@sumatra.local> * external/yottalab.py: New file containing Roberto Bucher's control library functions. OK to start pulling these into the main library, with attribution, but note that they use modifications of the default library => some rewrites will be needed. 2010-09-11 Richard Murray <murray@sumatra.local> * src/matlab.py (step): Added local step response function that uses lsim2() instead of signal.step (which can't handle integrators). This function may not be needed when new scipy step2() function is available. (impulse): Added local impulse response function that sets the initial condition based on the input matrix and then uses the lsim2() function to compute the response. * examples/test-response.py: Added test script for making sure that time repsonse functions are working as desired * src/matlab.py (lsim): Added local version of lsim that calls signal.lsim2 (actual ODE integrator) Modified Paths: -------------- trunk/ChangeLog trunk/Pending trunk/src/__init__.py trunk/src/matlab.py trunk/src/rlocus.py trunk/src/statefbk.py Added Paths: ----------- trunk/external/yottalab.py trunk/src/delay.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2010-06-18 05:10:26 UTC (rev 28) +++ trunk/ChangeLog 2010-11-06 13:03:55 UTC (rev 29) @@ -1,3 +1,45 @@ +2010-11-05 Richard Murray <murray@sumatra.local> + + * external/yottalab.py: New file containing Roberto Bucher's control + library functions. OK to start pulling these into the main library, + with attribution, but note that they use modifications of the + default library => some rewrites will be needed. + +2010-09-11 Richard Murray <murray@sumatra.local> + + * src/matlab.py (step): Added local step response function that uses + lsim2() instead of signal.step (which can't handle integrators). + This function may not be needed when new scipy step2() function is + available. + (impulse): Added local impulse response function that sets the + initial condition based on the input matrix and then uses the + lsim2() function to compute the response. + + * examples/test-response.py: Added test script for making sure that + time repsonse functions are working as desired + + * src/matlab.py (lsim): Added local version of lsim that calls + signal.lsim2 (actual ODE integrator) + +2010-09-06 Richard Murray <murray@sumatra.local> + + * src/statefbk.py (ctrb): new function for testing controllability + * src/statefbk.py (obsv): new function for testing observabiilty + +2010-09-02 Richard Murray <murray@sumatra.local> + + * src/statefbk.py (place): Use np.size() instead of len() for + finding length of placed_eigs for better compatability with + different python versions [courtesy of Roberto Bucher] + + * src/delay.py (pade): New file for delay-based computations + + initial implementation of pade() [courtesy Sawyer Fuller] + +2010-06-17 Richard Murray <murray@sumatra.local> + + * src/rlocus.py: changed num, den to nump, denp for clarity + * src/rlocus.py: new file with Ryan Krauss's root locus code + 2010-06-06 Richard Murray <murray@sumatra.local> * examples/pvtol-lqr.py: Added example to test out LQR routines Modified: trunk/Pending =================================================================== --- trunk/Pending 2010-06-18 05:10:26 UTC (rev 28) +++ trunk/Pending 2010-11-06 13:03:55 UTC (rev 29) @@ -1,8 +1,42 @@ List of Pending changes for control-python RMM, 5 Sep 09 +This file contains brief notes on features that need to be added to +the python control library. Mainly intended to keep track of "bigger +picture" things that need to be done. + +--> See src/matlab.py for a list of MATLAB functions that eventually need + to be implemented. + +OPEN BUGS + * step() doesn't handle systems with a pole at the origin (use lsim2) + +Transfer code from Roberto Bucher's yottalab to python-control + acker - pole placement using Ackermann method + c2d - contimous to discrete time conversion + full_obs - full order observer + red_obs - reduced order observer + comp_form - state feedback controller+observer in compact form + comp_form_i - state feedback controller+observer+integ in compact form + dsimul - simulate discrete time systems + dstep - step response (plot) of discrete time systems + dimpulse - imoulse response (plot) of discrete time systems + bb_step - step response (plot) of continous time systems + sysctr - system+controller+observer+feedback + care - Solve Riccati equation for contimous time systems + dare - Solve Riccati equation for discrete time systems + dlqr - discrete linear quadratic regulator + minreal - minimal state space representation + +Transfer code from Ryan Krauss's control.py to python-control + * phase margin computations (as part of margin command) + * step reponse + * c2d, c2d_tustin (compare to Bucher version first) + Examples and test cases + * Put together unit tests for all functions (after deciding on framework) * Figure out how to import 'figure' command properly (version issue?) + * Figure out source of BadCoefficients warning messages (pvtol-lqr and others) TransferFunction class fixes * evalfr is not working (num, den stored as ndarrays, not poly1ds) @@ -16,12 +50,11 @@ * Implement pzmap for state space systems LTI updates - * Try to get control.matlab.step semantics to be closer to MATLAB + * Implement control.matlab.step (with semantics similar to MATLAB) Basic functions to be added * margin - compute gain and phase margin (no plot) * lqr - compute optimal feedback gains (use SLICOT SB02ND.f) - * lqe - compute optimal feedback gains (use SLICOT SB02ND.f) * lyap - solve Lyapunov equation (use SLICOT SB03MD.f) * See http://www.slicot.org/shared/libindex.html for list of functions Added: trunk/external/yottalab.py =================================================================== --- trunk/external/yottalab.py (rev 0) +++ trunk/external/yottalab.py 2010-11-06 13:03:55 UTC (rev 29) @@ -0,0 +1,652 @@ +""" +This is a procedural interface to the yttalab library + +ro...@su... + +The following commands are provided: + +Design and plot commands + acker - pole placement using Ackermann method + c2d - contimous to discrete time conversion + full_obs - full order observer + red_obs - reduced order observer + comp_form - state feedback controller+observer in compact form + comp_form_i - state feedback controller+observer+integ in compact form + dsimul - simulate discrete time systems + dstep - step response (plot) of discrete time systems + dimpulse - imoulse response (plot) of discrete time systems + bb_step - step response (plot) of continous time systems + sysctr - system+controller+observer+feedback + care - Solve Riccati equation for contimous time systems + dare - Solve Riccati equation for discrete time systems + dlqr - discrete linear quadratic regulator + minreal - minimal state space representation + +""" +from matplotlib.pylab import * +from control.matlab import * +from numpy import hstack,vstack,pi +from scipy import zeros,ones,eye,mat,shape,size,size, \ + arange,real,poly,array,diag +from scipy.linalg import det,inv,expm,eig,eigvals +import numpy as np +import scipy as sp +from slycot import sb02od, tb03ad + +def acker(A,B,poles): + """Pole placemenmt using Ackermann method + + Call: + k=acker(A,B,poles) + + Parameters + ---------- + A, B : State and input matrix of the system + poles: desired poles + + Returns + ------- + k: matrix + State feedback gains + + """ + a=mat(A) + b=mat(B) + p=real(poly(poles)) + ct=ctrb(A,B) + if det(ct)==0: + k=0 + print "Pole placement invalid" + else: + n=size(p) + pmat=p[n-1]*a**0 + for i in arange(1,n): + pmat=pmat+p[n-i-1]*a**i + k=inv(ct)*pmat + k=k[-1][:] + return k + +def c2d(sys,Ts): + """Continous to discrete conversion with ZOH method + + Call: + sysd=c2d(sys,Ts) + + Parameters + ---------- + sys : System in statespace or Tf form + Ts: Sampling Time + + Returns + ------- + sysd: ss or Tf system + Discrete system + + """ + flag = 0 + if type(sys).__name__=='TransferFunction': + sys=tf2ss(sys) + flag=1 + a=sys.A + b=sys.B + c=sys.C + d=sys.D + n=shape(a)[0] + nb=shape(b)[1] + ztmp=zeros((nb,n+nb)) + tmp=hstack((a,b)) + tmp=vstack((tmp,ztmp)) + tmp=expm(tmp*Ts) + a=tmp[0:n,0:n] + b=tmp[0:n,n:n+nb] + sysd=ss(a,b,c,d,Ts) + if flag==1: + sysd=ss2tf(sysd) + return sysd + +def full_obs(sys,poles): + """Full order observer of the system sys + + Call: + obs=full_obs(sys,poles) + + Parameters + ---------- + sys : System in State Space form + poles: desired observer poles + + Returns + ------- + obs: ss + Observer + + """ + if type(sys).__name__=='TransferFunction': + "System must be in state space form" + return + a=mat(sys.A) + b=mat(sys.B) + c=mat(sys.C) + d=mat(sys.D) + poles=mat(poles) + L=place(a.T,c.T,poles) + L=mat(L).T + Ao=a-L*c + Bo=hstack((b-L*d,L)) + n=shape(Ao) + m=shape(Bo) + Co=eye(n[0],n[1]) + Do=zeros((n[0],m[1])) + obs=ss(Ao,Bo,Co,Do,sys.Tsamp) + return obs + +def red_obs(sys,T,poles): + """Reduced order observer of the system sys + + Call: + obs=red_obs(sys,T,poles) + + Parameters + ---------- + sys : System in State Space form + T: Complement matrix + poles: desired observer poles + + Returns + ------- + obs: ss + Reduced order Observer + + """ + if type(sys).__name__=='TransferFunction': + "System must be in state space form" + return + a=mat(sys.A) + b=mat(sys.B) + c=mat(sys.C) + d=mat(sys.D) + T=mat(T) + P=mat(vstack((c,T))) + poles=mat(poles) + invP=inv(P) + AA=P*a*invP + ny=shape(c)[0] + nx=shape(a)[0] + nu=shape(b)[1] + + A11=AA[0:ny,0:ny] + A12=AA[0:ny,ny:nx] + A21=AA[ny:nx,0:ny] + A22=AA[ny:nx,ny:nx] + + L1=place(A22.T,A12.T,poles) + L1=mat(L1).T + + nn=nx-ny + + tmp1=mat(hstack((-L1,eye(nn,nn)))) + tmp2=mat(vstack((zeros((ny,nn)),eye(nn,nn)))) + Ar=tmp1*P*a*invP*tmp2 + + tmp3=vstack((eye(ny,ny),L1)) + tmp3=mat(hstack((P*b,P*a*invP*tmp3))) + tmp4=hstack((eye(nu,nu),zeros((nu,ny)))) + tmp5=hstack((-d,eye(ny,ny))) + tmp4=mat(vstack((tmp4,tmp5))) + + Br=tmp1*tmp3*tmp4 + + Cr=invP*tmp2 + + tmp5=hstack((zeros((ny,nu)),eye(ny,ny))) + tmp6=hstack((zeros((nn,nu)),L1)) + tmp5=mat(vstack((tmp5,tmp6))) + Dr=invP*tmp5*tmp4 + + obs=ss(Ar,Br,Cr,Dr,sys.Tsamp) + return obs + +def comp_form(sys,obs,K): + """Compact form Conroller+Observer + + Call: + contr=comp_form(sys,obs,K) + + Parameters + ---------- + sys : System in State Space form + obs : Observer in State Space form + K: State feedback gains + + Returns + ------- + contr: ss + Controller + + """ + nx=shape(sys.A)[0] + ny=shape(sys.C)[0] + nu=shape(sys.B)[1] + no=shape(obs.A)[0] + + Bu=mat(obs.B[:,0:nu]) + By=mat(obs.B[:,nu:]) + Du=mat(obs.D[:,0:nu]) + Dy=mat(obs.D[:,nu:]) + + X=inv(eye(nu,nu)+K*Du) + + Ac = mat(obs.A)-Bu*X*K*mat(obs.C); + Bc = hstack((Bu*X,By-Bu*X*K*Dy)) + Cc = -X*K*mat(obs.C); + Dc = hstack((X,-X*K*Dy)) + contr = ss(Ac,Bc,Cc,Dc,sys.Tsamp) + return contr + +def comp_form_i(sys,obs,K,Ts,Cy=[[1]]): + """Compact form Conroller+Observer+Integral part + Only for discrete systems!!! + + Call: + contr=comp_form_i(sys,obs,K,Ts[,Cy]) + + Parameters + ---------- + sys : System in State Space form + obs : Observer in State Space form + K: State feedback gains + Ts: Sampling time + Cy: feedback matric to choose the output for integral part + + Returns + ------- + contr: ss + Controller + + """ + if sys.Tsamp==0.0: + print "contr_form_i works only with discrete systems!" + return + + ny=shape(sys.C)[0] + nu=shape(sys.B)[1] + nx=shape(sys.A)[0] + no=shape(obs.A)[0] + ni=shape(Cy)[0] + + B_obsu = mat(obs.B[:,0:nu]) + B_obsy = mat(obs.B[:,nu:nu+ny]) + D_obsu = mat(obs.D[:,0:nu]) + D_obsy = mat(obs.D[:,nu:nu+ny]) + + k=mat(K) + nk=shape(k)[1] + Ke=k[:,nk-ni:] + K=k[:,0:nk-ni] + X = inv(eye(nu,nu)+K*D_obsu); + + a=mat(obs.A) + c=mat(obs.C) + Cy=mat(Cy) + + tmp1=hstack((a-B_obsu*X*K*c,-B_obsu*X*Ke)) + + tmp2=hstack((zeros((ni,no)),eye(ni,ni))) + A_ctr=vstack((tmp1,tmp2)) + + tmp1=hstack((zeros((no,ni)),-B_obsu*X*K*D_obsy+B_obsy)) + tmp2=hstack((eye(ni,ni)*Ts,-Cy*Ts)) + B_ctr=vstack((tmp1,tmp2)) + + C_ctr=hstack((-X*K*c,-X*Ke)) + D_ctr=hstack((zeros((nu,ni)),-X*K*D_obsy)) + + contr=ss(A_ctr,B_ctr,C_ctr,D_ctr,sys.Tsamp) + return contr + +def dsimul(sys,u): + """Simulate the discrete system sys + Only for discrete systems!!! + + Call: + y=dsimul(sys,u) + + Parameters + ---------- + sys : Discrete System in State Space form + u : input vector + Returns + ------- + y: ndarray + Simulation results + + """ + a=mat(sys.A) + b=mat(sys.B) + c=mat(sys.C) + d=mat(sys.D) + nx=shape(a)[0] + ns=shape(u)[1] + xk=zeros((nx,1)) + for i in arange(0,ns): + uk=u[:,i] + xk_1=a*xk+b*uk + yk=c*xk+d*uk + xk=xk_1 + if i==0: + y=yk + else: + y=hstack((y,yk)) + y=array(y).T + return y + +def dstep(sys,Tf=10.0): + """Plot the step response of the discrete system sys + Only for discrete systems!!! + + Call: + y=dstep(sys, [,Tf=final time])) + + Parameters + ---------- + sys : Discrete System in State Space form + Tf : Final simulation time + + Returns + ------- + Nothing + + """ + Ts=sys.Tsamp + if Ts==0.0: + "Only discrete systems allowed!" + return + + ns=int(Tf/Ts+1) + u=ones((1,ns)) + y=dsimul(sys,u) + T=arange(0,Tf+Ts/2,Ts) + plot(T,y) + grid() + show() + +def dimpulse(sys,Tf=10.0): + """Plot the impulse response of the discrete system sys + Only for discrete systems!!! + + Call: + y=dimpulse(sys,[,Tf=final time])) + + Parameters + ---------- + sys : Discrete System in State Space form + Tf : Final simulation time + + Returns + ------- + Nothing + + """ + Ts=sys.Tsamp + if Ts==0.0: + "Only discrete systems allowed!" + return + + ns=int(Tf/Ts+1) + u=zeros((1,ns)) + u[0,0]=1/Ts + y=dsimul(sys,u) + T=arange(0,Tf+Ts/2,Ts) + plot(T,y) + grid() + show() + +# Step response (plot) +def bb_step(sys,X0=None,Tf=None,Ts=0.001): + """Plot the step response of the continous system sys + + Call: + y=bb_step(sys [,Tf=final time] [,Ts=time step]) + + Parameters + ---------- + sys : Continous System in State Space form + X0: Initial state vector (not used yet) + Ts : sympling time + Tf : Final simulation time + + Returns + ------- + Nothing + + """ + if Tf==None: + vals = eigvals(sys.A) + r = min(abs(real(vals))) + if r == 0.0: + r = 1.0 + Tf = 7.0 / r + sysd=c2d(sys,Ts) + dstep(sysd,Tf=Tf) + +def sysctr(sys,contr): + """Build the discrete system controller+plant+output feedback + + Call: + syscontr=sysctr(sys,contr) + + Parameters + ---------- + sys : Continous System in State Space form + contr: Controller (with observer if required) + + Returns + ------- + sysc: ss system + The system with reference as input and outputs of plants + as output + + """ + if contr.Tsamp!=sys.Tsamp: + print "Systems with different sampling time!!!" + return + sysf=contr*sys + + nu=shape(sysf.B)[1] + b1=mat(sysf.B[:,0]) + b2=mat(sysf.B[:,1:nu]) + d1=mat(sysf.D[:,0]) + d2=mat(sysf.D[:,1:nu]) + + n2=shape(d2)[0] + + Id=mat(eye(n2,n2)) + X=inv(Id-d2) + + Af=mat(sysf.A)+b2*X*mat(sysf.C) + Bf=b1+b2*X*d1 + Cf=X*mat(sysf.C) + Df=X*d1 + + sysc=ss(Af,Bf,Cf,Df,sys.Tsamp) + return sysc + +def care(A,B,Q,R): + """Solve Riccati equation for discrete time systems + + Usage + ===== + [K, S, E] = care(A, B, Q, R) + + Inputs + ------ + A, B: 2-d arrays with dynamics and input matrices + sys: linear I/O system + Q, R: 2-d array with state and input weight matrices + + Outputs + ------- + X: solution of the Riccati eq. + """ + + # Check dimensions for consistency + nstates = B.shape[0]; + ninputs = B.shape[1]; + if (A.shape[0] != nstates or A.shape[1] != nstates): + raise ControlDimension("inconsistent system dimensions") + + elif (Q.shape[0] != nstates or Q.shape[1] != nstates or + R.shape[0] != ninputs or R.shape[1] != ninputs) : + raise ControlDimension("incorrect weighting matrix dimensions") + + rcond,X,w,S,T = \ + sb02od(nstates, ninputs, A, B, Q, R, 'C'); + + return X + + +def dare(A,B,Q,R): + """Solve Riccati equation for discrete time systems + + Usage + ===== + [K, S, E] = care(A, B, Q, R) + + Inputs + ------ + A, B: 2-d arrays with dynamics and input matrices + sys: linear I/O system + Q, R: 2-d array with state and input weight matrices + + Outputs + ------- + X: solution of the Riccati eq. + """ + + # Check dimensions for consistency + nstates = B.shape[0]; + ninputs = B.shape[1]; + if (A.shape[0] != nstates or A.shape[1] != nstates): + raise ControlDimension("inconsistent system dimensions") + + elif (Q.shape[0] != nstates or Q.shape[1] != nstates or + R.shape[0] != ninputs or R.shape[1] != ninputs) : + raise ControlDimension("incorrect weighting matrix dimensions") + + rcond,X,w,S,T = \ + sb02od(nstates, ninputs, A, B, Q, R, 'D'); + + return X + + +def dlqr(*args, **keywords): + """Linear quadratic regulator design for discrete systems + + Usage + ===== + [K, S, E] = dlqr(A, B, Q, R, [N]) + [K, S, E] = dlqr(sys, Q, R, [N]) + + The dlqr() function computes the optimal state feedback controller + that minimizes the quadratic cost + + J = \sum_0^\infty x' Q x + u' R u + 2 x' N u + + Inputs + ------ + A, B: 2-d arrays with dynamics and input matrices + sys: linear I/O system + Q, R: 2-d array with state and input weight matrices + N: optional 2-d array with cross weight matrix + + Outputs + ------- + K: 2-d array with state feedback gains + S: 2-d array with solution to Riccati equation + E: 1-d array with eigenvalues of the closed loop system + """ + + # + # Process the arguments and figure out what inputs we received + # + + # Get the system description + if (len(args) < 3): + raise ControlArgument("not enough input arguments") + + elif (ctrlutil.issys(args[0])): + # We were passed a system as the first argument; extract A and B + A = array(args[0].A, ndmin=2, dtype=float); + B = array(args[0].B, ndmin=2, dtype=float); + index = 1; + if args[0].Tsamp==0.0: + print "dlqr works only for discrete systems!" + return + else: + # Arguments should be A and B matrices + A = array(args[0], ndmin=2, dtype=float); + B = array(args[1], ndmin=2, dtype=float); + index = 2; + + # Get the weighting matrices (converting to matrices, if needed) + Q = array(args[index], ndmin=2, dtype=float); + R = array(args[index+1], ndmin=2, dtype=float); + if (len(args) > index + 2): + N = array(args[index+2], ndmin=2, dtype=float); + else: + N = zeros((Q.shape[0], R.shape[1])); + + # Check dimensions for consistency + nstates = B.shape[0]; + ninputs = B.shape[1]; + if (A.shape[0] != nstates or A.shape[1] != nstates): + raise ControlDimension("inconsistent system dimensions") + + elif (Q.shape[0] != nstates or Q.shape[1] != nstates or + R.shape[0] != ninputs or R.shape[1] != ninputs or + N.shape[0] != nstates or N.shape[1] != ninputs): + raise ControlDimension("incorrect weighting matrix dimensions") + + #Solve the riccati equation + X = dare(A,B,Q,R) + + # Now compute the return value + Phi=mat(A) + H=mat(B) + K=inv(H.T*X*H+R)*(H.T*X*Phi) + L=eig(Phi-H*K) + return K,X,L + +def minreal(sys): + """Minimal representation for state space systems + + Usage + ===== + [sysmin]=minreal[sys] + + Inputs + ------ + + sys: system in ss or tf form + + Outputs + ------- + sysfin: system in state space form + """ + a=mat(sys.A) + b=mat(sys.B) + c=mat(sys.C) + d=mat(sys.D) + nx=shape(a)[0] + ni=shape(b)[1] + no=shape(c)[0] + + out=tb03ad(nx,no,ni,a,b,c,d,'R') + + nr=out[3] + A=out[0][:nr,:nr] + B=out[1][:nr,:ni] + C=out[2][:no,:nr] + sysf=ss(A,B,C,sys.D,sys.Tsamp) + return sysf + Modified: trunk/src/__init__.py =================================================================== --- trunk/src/__init__.py 2010-06-18 05:10:26 UTC (rev 28) +++ trunk/src/__init__.py 2010-11-06 13:03:55 UTC (rev 29) @@ -61,3 +61,4 @@ from freqplot import * from bdalg import * from statefbk import * +from delay import * Added: trunk/src/delay.py =================================================================== --- trunk/src/delay.py (rev 0) +++ trunk/src/delay.py 2010-11-06 13:03:55 UTC (rev 29) @@ -0,0 +1,68 @@ +# delay.py - functions involving time delays +# +# Author: Sawyer Fuller +# Date: 26 Aug 2010 +# +# This file contains functions for implementing time delays (currently +# only the pade() function). +# +# Copyright (c) 2010 by California Institute of Technology +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the California Institute of Technology nor +# the names of its contributors may be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH +# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $Id: pade.py 17 2010-05-29 23:50:52Z murrayrm $ + +def pade(T, n=1): + """ + Return the numerator and denominator coefficients of the Pade approximation. + + Inputs: + T --> time delay + n --> order of approximation + + Outputs: + num, den --> arrays in descending powers of s. + + Based on an algorithm in Golub and van Loan, "Matrix Computation" 3rd. + Ed. pp. 572-574. + """ + num = [0. for i in range(n+1)] + num[-1] = 1. + den = [0. for i in range(n+1)] + den[-1] = 1. + c = 1. + for k in range(1, n+1): + c = T * c * (n - k + 1)/(2 * n - k + 1)/k + num[n - k] = c * (-1)**k + den[n - k] = c + num = [coeff/den[0] for coeff in num] + den = [coeff/den[0] for coeff in den] + return num, den Modified: trunk/src/matlab.py =================================================================== --- trunk/src/matlab.py 2010-06-18 05:10:26 UTC (rev 28) +++ trunk/src/matlab.py 2010-11-06 13:03:55 UTC (rev 29) @@ -55,7 +55,6 @@ # Import MATLAB-like functions that are defined in other packages from scipy.signal import zpk2ss, ss2zpk, tf2zpk, zpk2tf -from scipy.signal import lsim, impulse, step from scipy import linspace, logspace # Control system library @@ -70,7 +69,8 @@ from freqplot import nyquist, gangof4 from bdalg import series, parallel, negate, feedback from pzmap import pzmap -from statefbk import place, lqr +from statefbk import ctrb, obsv, place, lqr +from delay import pade __doc__ = """ The control.matlab module defines functions that are roughly the @@ -191,8 +191,8 @@ drss - random stable discrete-time state-space models ss2ss - state coordinate transformation canon - canonical forms of state-space models - ctrb - controllability matrix - obsv - observability matrix +* ctrb - controllability matrix +* obsv - observability matrix gram - controllability and observability gramians ss/prescale - optimal scaling of state-space models. balreal - gramian-based input/output balancing @@ -214,7 +214,7 @@ lti/hasdelay - true for models with time delays lti/totaldelay - total delay between each input/output pair lti/delay2z - replace delays by poles at z=0 or FRD phase shift - pade - pade approximation of time delays +* pade - pade approximation of time delays Model dimensions and characteristics class - model type ('tf', 'zpk', 'ss', or 'frd') @@ -370,3 +370,89 @@ # Call the bode command return freqplot.bode(syslist, omega, **keywords) + +# +# Modifications to scipy.signal functions +# + +# Redefine lsim to use lsim2 +def lsim(*args, **keywords): + """Simulate the output of a linear system + + Usage + ===== + (T, yout, xout) = lsim(sys, u, T, X0) + + Inputs: + sys LTI system + u input array giving input at each time T + T time steps at which the input is defined + X0 initial condition (optional, default = 0) + + Outputs: + T time values of the output + yout response of the system + xout time evolution of the state vector + """ + return sp.signal.lsim2(*args, **keywords) + +#! Redefine step to use lsim2 +#! Not yet implemented +def step(*args, **keywords): + """Step response of a linear system + + Usage + ===== + (T, yout) = step(sys, T, X0) + + Inputs: + sys LTI system + T time steps (optional; autocomputed if not gien) + X0 initial condition (optional, default = 0) + + Outputs: + T time values of the output + yout response of the system + """ + return sp.signal.step(*args, **keywords) + +# Redefine initial to use lsim2 +#! Not yet implemented (uses step for now) +def initial(*args, **keywords): + """Initial condition response of a linear system + + Usage + ===== + (T, yout) = initial(sys, T, X0) + + Inputs: + sys LTI system + T time steps (optional; autocomputed if not gien) + X0 initial condition (optional, default = 0) + + Outputs: + T time values of the output + yout response of the system + """ + return sp.signal.initial(*args, **keywords) + +# Redefine impulse to use initial() +#! Not yet implemented (uses impulse for now) +def impulse(*args, **keywords): + """Step response of a linear system + + Usage + ===== + (T, yout) = impulse(sys, T, X0) + + Inputs: + sys LTI system + T time steps (optional; autocomputed if not gien) + X0 initial condition (optional, default = 0) + + Outputs: + T time values of the output + yout response of the system + """ + return sp.signal.impulse(*args, **keywords) + Modified: trunk/src/rlocus.py =================================================================== --- trunk/src/rlocus.py 2010-06-18 05:10:26 UTC (rev 28) +++ trunk/src/rlocus.py 2010-11-06 13:03:55 UTC (rev 29) @@ -53,7 +53,7 @@ of kvect.""" # Convert numerator and denominator to polynomials if they aren't - (num, den) = _systopoly1d(sys); + (nump, denp) = _systopoly1d(sys); # Set up the figure if fig is None: @@ -68,11 +68,11 @@ mymat = _RLSortRoots(sys, mymat) # plot open loop poles - poles = array(den.r) + poles = array(denp.r) ax.plot(real(poles), imag(poles), 'x') # plot open loop zeros - zeros = array(num.r) + zeros = array(nump.r) if zeros.any(): ax.plot(real(zeros), imag(zeros), 'o') @@ -94,23 +94,23 @@ """Extract numerator and denominator polynomails for a system""" # Start by extracting the numerator and denominator from system object - num = sys.num; den = sys.den; + nump = sys.num; denp = sys.den; # Check to see if num, den are already polynomials; otherwise convert - if (not isinstance(num, poly1d)): num = poly1d(num) - if (not isinstance(den, poly1d)): den = poly1d(den) + if (not isinstance(nump, poly1d)): nump = poly1d(nump) + if (not isinstance(denp, poly1d)): denp = poly1d(denp) - return (num, den) + return (nump, denp) def _RLFindRoots(sys, kvect): """Find the roots for the root locus.""" # Convert numerator and denominator to polynomials if they aren't - (num, den) = _systopoly1d(sys); + (nump, denp) = _systopoly1d(sys); roots = [] for k in kvect: - curpoly = den + k * num + curpoly = denp + k * nump curroots = curpoly.r curroots.sort() roots.append(curroots) Modified: trunk/src/statefbk.py =================================================================== --- trunk/src/statefbk.py 2010-06-18 05:10:26 UTC (rev 28) +++ trunk/src/statefbk.py 2010-11-06 13:03:55 UTC (rev 29) @@ -1,6 +1,6 @@ # statefbk.py - tools for state feedback control # -# Author: Richard M. Murray +# Author: Richard M. Murray, Roberto Bucher # Date: 31 May 2010 # # This file contains routines for designing state space controllers @@ -92,7 +92,7 @@ # Call SLICOT routine to place the eigenvalues A_z,w,nfp,nap,nup,F,Z = \ - sb01bd(B_mat.shape[0], B_mat.shape[1], len(placed_eigs), alpha, + sb01bd(B_mat.shape[0], B_mat.shape[1], np.size(placed_eigs), alpha, A_mat, B_mat, placed_eigs, 'C'); # Return the gain matrix, with MATLAB gain convention @@ -184,3 +184,57 @@ E = w[0:nstates]; return K, S, E + +def ctrb(A,B): + """Controllabilty matrix + + Usage + ===== + Wc = ctrb(A, B) + + Inputs + ------ + A, B: Dynamics and input matrix of the system + + Outputs + ------- + Wc: Controllability matrix + """ + + # Convert input parameters to matrices (if they aren't already) + amat = np.mat(A) + bmat = np.mat(B) + n = np.shape(amat)[0] + + # Construct the controllability matrix + ctrb = bmat + for i in range(1, n): + ctrb = np.vstack((ctrb, amat**i*bmat)) + return ctrb + +def obsv(A, C): + """Observability matrix + + Usage + ===== + Wc = obsv(A, C) + + Inputs + ------ + A, C: Dynamics and output matrix of the system + + Outputs + ------- + Wc: Observability matrix + """ + + # Convert input parameters to matrices (if they aren't already) + amat = np.mat(A) + cmat = np.mat(C) + n = np.shape(amat)[0] + + # Construct the controllability matrix + obsv = cmat + for i in range(1, n): + obsv = np.hstack((obsv, cmat*amat**i)) + return obsv This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2011-02-13 03:32:19
|
Revision: 131 http://python-control.svn.sourceforge.net/python-control/?rev=131&view=rev Author: murrayrm Date: 2011-02-13 03:32:11 +0000 (Sun, 13 Feb 2011) Log Message: ----------- * Adding a few unit tests (will probably get redone in v0.4a) * Updated list of changes * Incremented version number to 0.3d This is intended to be final set of changes before moving over to v0.4a Modified Paths: -------------- trunk/ChangeLog trunk/setup.py Added Paths: ----------- trunk/examples/bdalg-matlab.py trunk/examples/test-statefbk.py Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-02-13 03:25:52 UTC (rev 130) +++ trunk/ChangeLog 2011-02-13 03:32:11 UTC (rev 131) @@ -1,3 +1,16 @@ +2011-02-12 Richard Murray <murray@sumatra.local> + + * setup.py: Updated version number to 0.3d, in preparation for release + + * src/statefbk.py (lqr): Updated sb02md calling signature to match + latest slycot version (Lauren Padilla) + + * src/freqplot.py (nichols_grid): new function from Allan McInnes + <all...@ca...> to generate a Nichols chart for a + given plot (equivalent to ngrid in MATLAB). + + * src/matlab.py (ngrid): MATLAB compatible ngrid() command + 2010-11-05 Richard Murray <murray@sumatra.local> * external/yottalab.py: New file containing Roberto Bucher's control Added: trunk/examples/bdalg-matlab.py =================================================================== --- trunk/examples/bdalg-matlab.py (rev 0) +++ trunk/examples/bdalg-matlab.py 2011-02-13 03:32:11 UTC (rev 131) @@ -0,0 +1,17 @@ +# bdalg-matlab.py - demonstrate some MATLAB commands for block diagram altebra +# RMM, 29 May 09 + +from control.matlab import * # MATLAB-like functions + +# System matrices +A1 = [[0, 1.], [-4, -1]] +B1 = [[0], [1.]] +C1 = [[1., 0]] +sys1ss = ss(A1, B1, C1, 0) +sys1tf = ss2tf(sys1ss) + +sys2tf = tf([1, 0.5], [1, 5]); +sys2ss = tf2ss(sys2tf); + +# Series composition +series1 = sys1ss + sys2ss; Added: trunk/examples/test-statefbk.py =================================================================== --- trunk/examples/test-statefbk.py (rev 0) +++ trunk/examples/test-statefbk.py 2011-02-13 03:32:11 UTC (rev 131) @@ -0,0 +1,27 @@ +# test-statefbk.py - Unit tests for state feedback code +# RMM, 6 Sep 2010 + +import numpy as np # Numerical library +from scipy import * # Load the scipy functions +from control.matlab import * # Load the controls systems library + +# Parameters defining the system +m = 250.0 # system mass +k = 40.0 # spring constant +b = 60.0 # damping constant + +# System matrices +A = matrix([[1, -1, 1.], [1, -k/m, -b/m], [1, 1, 1]]) +B = matrix([[0], [1/m], [1]]) +C = matrix([[1., 0, 1.]]) +sys = ss(A, B, C, 0); + +# Controllability +Wc = ctrb(A, B) +print "Wc = ", Wc + +# Observability +Wo = obsv(A, C) +print "Wo = ", Wo + + Modified: trunk/setup.py =================================================================== --- trunk/setup.py 2011-02-13 03:25:52 UTC (rev 130) +++ trunk/setup.py 2011-02-13 03:32:11 UTC (rev 131) @@ -3,7 +3,7 @@ from distutils.core import setup setup(name='control', - version='0.3c', + version='0.3d', description='Python Control Systems Library', author='Richard Murray', author_email='mu...@cd...', This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2011-04-02 19:32:40
|
Revision: 151 http://python-control.svn.sourceforge.net/python-control/?rev=151&view=rev Author: murrayrm Date: 2011-04-02 19:32:33 +0000 (Sat, 02 Apr 2011) Log Message: ----------- added examples/ and tests/ to source distribution; updated version number to 0.4c Modified Paths: -------------- trunk/ChangeLog trunk/README trunk/setup.py Added Paths: ----------- trunk/MANIFEST.in Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-04-02 18:34:21 UTC (rev 150) +++ trunk/ChangeLog 2011-04-02 19:32:33 UTC (rev 151) @@ -1,5 +1,10 @@ 2011-04-02 Richard Murray <murray@malabar.local> + * MANIFEST.in: Added MANIFEST.in file to include examples and tests + in source distribution + + * README: Updated to include information on how to run unit tests. + * setup.py: updated version number to 0.4c ---- control-0.4b released ----- Added: trunk/MANIFEST.in =================================================================== --- trunk/MANIFEST.in (rev 0) +++ trunk/MANIFEST.in 2011-04-02 19:32:33 UTC (rev 151) @@ -0,0 +1,2 @@ +include examples/*.py +include tests/*.py Modified: trunk/README =================================================================== --- trunk/README 2011-04-02 18:34:21 UTC (rev 150) +++ trunk/README 2011-04-02 19:32:33 UTC (rev 151) @@ -16,3 +16,10 @@ examples/secord-matlab.py (using ipython -pylab). It should generate a step response, Bode plot and Nyquist plot for a simple second order linear system. + +You can also run a set of unit tests to make sure that everything is +working correctly. After installation, run + + python tests/test_all.py + +from the source distribution directory. Modified: trunk/setup.py =================================================================== --- trunk/setup.py 2011-04-02 18:34:21 UTC (rev 150) +++ trunk/setup.py 2011-04-02 19:32:33 UTC (rev 151) @@ -2,13 +2,13 @@ from distutils.core import setup -setup(name='control', - version='0.4c', - description='Python Control Systems Library', - author='Richard Murray', - author_email='mu...@cd...', - url='http://python-control.sourceforge.net', - requires='scipy', - packages=['control'], - package_dir = {'control':'src'}, +setup(name = 'control', + version = '0.4c', + description = 'Python Control Systems Library', + author = 'Richard Murray', + author_email = 'mu...@cd...', + url = 'http://python-control.sourceforge.net', + requires = ['scipy', 'matplotlib'], + package_dir = {'control' : 'src'}, + packages = ['control'], ) This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mur...@us...> - 2011-06-22 18:59:20
|
Revision: 163 http://python-control.svn.sourceforge.net/python-control/?rev=163&view=rev Author: murrayrm Date: 2011-06-22 18:59:13 +0000 (Wed, 22 Jun 2011) Log Message: ----------- Small modifications to sphinx documentation: * Updated intro material and added link to wiki * Added version information to doc/conf.py * Got rid of path dependency for sphinx (requires proper path) Documentation for 0.4d is now posted at http://python-control.sf.net/manual Modified Paths: -------------- trunk/ChangeLog trunk/doc/Makefile trunk/doc/conf.py trunk/doc/intro.rst Modified: trunk/ChangeLog =================================================================== --- trunk/ChangeLog 2011-06-22 06:02:02 UTC (rev 162) +++ trunk/ChangeLog 2011-06-22 18:59:13 UTC (rev 163) @@ -1,3 +1,13 @@ +2011-06-22 Richard Murray <murray@malabar.local> + + * doc/intro.rst: Added a slightly more general introduction, with a + pointer to the python-control wiki (on sf.net) + + * doc/Makefile: Changed path to sphinx-build to assume it is in the + users path (as opposed to an explicit path) + + * doc/conf.py: Added release information into documentation file + 2011-06-21 Richard Murray <murray@malabar.local> * src/statesp.py (_mimo2siso): Moved function from matlab.py. Modified: trunk/doc/Makefile =================================================================== --- trunk/doc/Makefile 2011-06-22 06:02:02 UTC (rev 162) +++ trunk/doc/Makefile 2011-06-22 18:59:13 UTC (rev 163) @@ -3,7 +3,7 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = /Applications/Sphinx-1.0.6/sphinx-build.py +SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build Modified: trunk/doc/conf.py =================================================================== --- trunk/doc/conf.py 2011-06-22 06:02:02 UTC (rev 162) +++ trunk/doc/conf.py 2011-06-22 18:59:13 UTC (rev 163) @@ -57,9 +57,9 @@ # built documents. # # The short X.Y version. -version = '0.0' +version = '0.4d' # The full version, including alpha/beta/rc tags. -release = '0.0' +release = '0.4d' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. Modified: trunk/doc/intro.rst =================================================================== --- trunk/doc/intro.rst 2011-06-22 06:02:02 UTC (rev 162) +++ trunk/doc/intro.rst 2011-06-22 18:59:13 UTC (rev 163) @@ -3,15 +3,26 @@ ============ Welcome to the Python-Control project. -This is sample documentation and will include our Princeton University APC524 contribution. -We can incorporate any existing documentation as well. For example from R.Murray's earlier tex document: +The python-control package is a set of python classes and functions +that implement common operations for the analysis and design of +feedback control systems. The initial goal is to implement all of the +functionality required to work through the examples in the textbook +Feedback Systems by \xC5str\xF6m and Murray. A MATLAB compatibility package +(control.matlab) is available that provides functions corresponding to +the commands available in the MATLAB Control Systems Toolbox. -Differences from MATLAB ------------------------ -* You must include commas in vectors. So [1 2 3] must be [1, 2, 3]. -* Functions that return multiple arguments use tuples -* Can't use braces for collections; use tuples instead -* Transfer functions are only implemented for SISO systems (due to limitations in the underlying signals.lti class); use state space representations for MIMO systems. +In addition to the documentation here, there is a project wiki that +contains some addional informaiton about how to use the package +(including some detailed worked examples): + http://python-control.sourceforge.net +Some Differences from MATLAB +---------------------------- +* You must include commas in vectors. So [1 2 3] must be [1, 2, 3]. +* Functions that return multiple arguments use tuples +* Can't use braces for collections; use tuples instead +* Transfer functions are only implemented for SISO systems (due to + limitations in the underlying signals.lti class); use state space + representations for MIMO systems. This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |