Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
ctrb converts ndim=1 B correctly; ctrb & obsv check input shapes
  • Loading branch information
roryyorke committed Jan 19, 2025
commit 236c1d40a77f4ef1dafde17a2f189f90f5b83f09
35 changes: 23 additions & 12 deletions control/statefbk.py
Original file line number Diff line number Diff line change
Expand Up @@ -1098,19 +1098,26 @@ def ctrb(A, B, t=None):
"""

# Convert input parameters to matrices (if they aren't already)
amat = _ssmatrix(A)
bmat = _ssmatrix(B)
n = np.shape(amat)[0]
m = np.shape(bmat)[1]
A = _ssmatrix(A)
if np.asarray(B).ndim == 1 and len(B) == A.shape[0]:
B = _ssmatrix(B, axis=0)
else:
B = _ssmatrix(B)

n = A.shape[0]
m = B.shape[1]

_check_shape('A', A, n, n, square=True)
_check_shape('B', B, n, m)

if t is None or t > n:
t = n

# Construct the controllability matrix
ctrb = np.zeros((n, t * m))
ctrb[:, :m] = bmat
ctrb[:, :m] = B
for k in range(1, t):
ctrb[:, k * m:(k + 1) * m] = np.dot(amat, ctrb[:, (k - 1) * m:k * m])
ctrb[:, k * m:(k + 1) * m] = np.dot(A, ctrb[:, (k - 1) * m:k * m])

return _ssmatrix(ctrb)

Expand Down Expand Up @@ -1140,20 +1147,24 @@ def obsv(A, C, t=None):
"""

# Convert input parameters to matrices (if they aren't already)
amat = _ssmatrix(A)
cmat = _ssmatrix(C)
n = np.shape(amat)[0]
p = np.shape(cmat)[0]
A = _ssmatrix(A)
C = _ssmatrix(C)

n = np.shape(A)[0]
p = np.shape(C)[0]

_check_shape('A', A, n, n, square=True)
_check_shape('C', C, p, n)

if t is None or t > n:
t = n

# Construct the observability matrix
obsv = np.zeros((t * p, n))
obsv[:p, :] = cmat
obsv[:p, :] = C

for k in range(1, t):
obsv[k * p:(k + 1) * p, :] = np.dot(obsv[(k - 1) * p:k * p, :], amat)
obsv[k * p:(k + 1) * p, :] = np.dot(obsv[(k - 1) * p:k * p, :], A)

return _ssmatrix(obsv)

Expand Down
34 changes: 34 additions & 0 deletions control/tests/statefbk_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ def testCtrbT(self):
Wc = ctrb(A, B, t=t)
np.testing.assert_array_almost_equal(Wc, Wctrue)

def testCtrbNdim1(self):
# gh-1097: treat 1-dim B as nx1
A = np.array([[1., 2.], [3., 4.]])
B = np.array([5., 7.])
Wctrue = np.array([[5., 19.], [7., 43.]])
Wc = ctrb(A, B)
np.testing.assert_array_almost_equal(Wc, Wctrue)

def testCtrbRejectMismatch(self):
# gh-1097: check A, B for compatible shapes
with pytest.raises(ControlDimension, match='A must be a square matrix'):
ctrb([[1,2]],[1])
with pytest.raises(ControlDimension, match='Incompatible dimensions of B matrix'):
ctrb([[1,2],[2,3]], 1)
with pytest.raises(ControlDimension, match='Incompatible dimensions of B matrix'):
ctrb([[1,2],[2,3]], [[1,2]])

def testObsvSISO(self):
A = np.array([[1., 2.], [3., 4.]])
C = np.array([[5., 7.]])
Expand All @@ -79,6 +96,23 @@ def testObsvT(self):
Wo = obsv(A, C, t=t)
np.testing.assert_array_almost_equal(Wo, Wotrue)

def testObsvNdim1(self):
# gh-1097: treat 1-dim C as 1xn
A = np.array([[1., 2.], [3., 4.]])
C = np.array([5., 7.])
Wotrue = np.array([[5., 7.], [26., 38.]])
Wo = obsv(A, C)
np.testing.assert_array_almost_equal(Wo, Wotrue)

def testObsvRejectMismatch(self):
# gh-1097: check A, B for compatible shapes
Comment thread
murrayrm marked this conversation as resolved.
Outdated
with pytest.raises(ControlDimension, match='A must be a square matrix'):
obsv([[1,2]],[1])
with pytest.raises(ControlDimension, match='Incompatible dimensions of C matrix'):
obsv([[1,2],[2,3]], 1)
with pytest.raises(ControlDimension, match='Incompatible dimensions of C matrix'):
obsv([[1,2],[2,3]], [[1],[2]])

def testCtrbObsvDuality(self):
A = np.array([[1.2, -2.3], [3.4, -4.5]])
B = np.array([[5.8, 6.9], [8., 9.1]])
Expand Down