Skip to content

Commit 67ea154

Browse files
committed
allow passthru system inputs in create_statefbk_iosystem
1 parent 935ae6f commit 67ea154

File tree

5 files changed

+212
-58
lines changed

5 files changed

+212
-58
lines changed

control/iosys.py

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,13 +1373,11 @@ def unused_signals(self):
13731373
-------
13741374
13751375
unused_inputs : dict
1376-
13771376
A mapping from tuple of indices (isys, isig) to string
13781377
'{sys}.{sig}', for all unused subsystem inputs.
13791378
13801379
unused_outputs : dict
1381-
1382-
A mapping from tuple of indices (isys, isig) to string
1380+
A mapping from tuple of indices (osys, osig) to string
13831381
'{sys}.{sig}', for all unused subsystem outputs.
13841382
13851383
"""
@@ -1433,10 +1431,13 @@ def _find_outputs_by_basename(self, basename):
14331431
for sig, isig in sys.output_index.items()
14341432
if sig == (basename)}
14351433

1436-
def check_unused_signals(self, ignore_inputs=None, ignore_outputs=None):
1434+
def check_unused_signals(
1435+
self, ignore_inputs=None, ignore_outputs=None, warning=True):
14371436
"""Check for unused subsystem inputs and outputs
14381437
1439-
If any unused inputs or outputs are found, emit a warning.
1438+
Check to see if there are any unused signals and return a list of
1439+
unused input and output signal descriptions. If `warning` is True
1440+
and any unused inputs or outputs are found, emit a warning.
14401441
14411442
Parameters
14421443
----------
@@ -1454,6 +1455,16 @@ def check_unused_signals(self, ignore_inputs=None, ignore_outputs=None):
14541455
If the 'sig' form is used, all subsystem outputs with that
14551456
name are considered ignored.
14561457
1458+
Returns
1459+
-------
1460+
dropped_inputs: list of tuples
1461+
A list of the dropped input signals, with each element of the
1462+
list in the form of (isys, isig).
1463+
1464+
dropped_outputs: list of tuples
1465+
A list of the dropped output signals, with each element of the
1466+
list in the form of (osys, osig).
1467+
14571468
"""
14581469

14591470
if ignore_inputs is None:
@@ -1477,7 +1488,7 @@ def check_unused_signals(self, ignore_inputs=None, ignore_outputs=None):
14771488
ignore_input_map[self._parse_signal(
14781489
ignore_input, 'input')[:2]] = ignore_input
14791490

1480-
# (isys, isig) -> signal-spec
1491+
# (osys, osig) -> signal-spec
14811492
ignore_output_map = {}
14821493
for ignore_output in ignore_outputs:
14831494
if isinstance(ignore_output, str) and '.' not in ignore_output:
@@ -1496,30 +1507,32 @@ def check_unused_signals(self, ignore_inputs=None, ignore_outputs=None):
14961507
used_ignored_inputs = set(ignore_input_map) - set(unused_inputs)
14971508
used_ignored_outputs = set(ignore_output_map) - set(unused_outputs)
14981509

1499-
if dropped_inputs:
1510+
if warning and dropped_inputs:
15001511
msg = ('Unused input(s) in InterconnectedSystem: '
15011512
+ '; '.join(f'{inp}={unused_inputs[inp]}'
15021513
for inp in dropped_inputs))
15031514
warn(msg)
15041515

1505-
if dropped_outputs:
1516+
if warning and dropped_outputs:
15061517
msg = ('Unused output(s) in InterconnectedSystem: '
15071518
+ '; '.join(f'{out} : {unused_outputs[out]}'
15081519
for out in dropped_outputs))
15091520
warn(msg)
15101521

1511-
if used_ignored_inputs:
1522+
if warning and used_ignored_inputs:
15121523
msg = ('Input(s) specified as ignored is (are) used: '
15131524
+ '; '.join(f'{inp} : {ignore_input_map[inp]}'
15141525
for inp in used_ignored_inputs))
15151526
warn(msg)
15161527

1517-
if used_ignored_outputs:
1528+
if warning and used_ignored_outputs:
15181529
msg = ('Output(s) specified as ignored is (are) used: '
15191530
+ '; '.join(f'{out}={ignore_output_map[out]}'
15201531
for out in used_ignored_outputs))
15211532
warn(msg)
15221533

1534+
return dropped_inputs, dropped_outputs
1535+
15231536

15241537
class LinearICSystem(InterconnectedSystem, LinearIOSystem):
15251538

@@ -2580,9 +2593,10 @@ def tf2io(*args, **kwargs):
25802593

25812594

25822595
# Function to create an interconnected system
2583-
def interconnect(syslist, connections=None, inplist=None, outlist=None,
2584-
params=None, check_unused=True, ignore_inputs=None,
2585-
ignore_outputs=None, warn_duplicate=None, **kwargs):
2596+
def interconnect(
2597+
syslist, connections=None, inplist=None, outlist=None, params=None,
2598+
check_unused=True, add_unused=False, ignore_inputs=None,
2599+
ignore_outputs=None, warn_duplicate=None, **kwargs):
25862600
"""Interconnect a set of input/output systems.
25872601
25882602
This function creates a new system that is an interconnection of a set of
@@ -2653,8 +2667,8 @@ def interconnect(syslist, connections=None, inplist=None, outlist=None,
26532667
the system input connects to only one subsystem input, a single input
26542668
specification can be given (without the inner list).
26552669
2656-
If omitted, the input map can be specified using the
2657-
:func:`~control.InterconnectedSystem.set_input_map` method.
2670+
If omitted the `input` parameter will be used to identify the list
2671+
of input signals to the overall system.
26582672
26592673
outlist : list of output connections, optional
26602674
List of connections for how the outputs from the subsystems are mapped
@@ -2886,10 +2900,30 @@ def interconnect(syslist, connections=None, inplist=None, outlist=None,
28862900
outlist = new_outlist
28872901

28882902
newsys = InterconnectedSystem(
2889-
syslist, connections=connections, inplist=inplist, outlist=outlist,
2890-
inputs=inputs, outputs=outputs, states=states,
2903+
syslist, connections=connections, inplist=inplist,
2904+
outlist=outlist, inputs=inputs, outputs=outputs, states=states,
28912905
params=params, dt=dt, name=name, warn_duplicate=warn_duplicate)
28922906

2907+
# See if we should add any signals
2908+
if add_unused:
2909+
# Get all unused signals
2910+
dropped_inputs, dropped_outputs = newsys.check_unused_signals(
2911+
ignore_inputs, ignore_outputs, warning=False)
2912+
2913+
# Add on any unused signals that we aren't ignoring
2914+
for isys, isig in dropped_inputs:
2915+
inplist.append((isys, isig))
2916+
inputs.append(newsys.syslist[isys].input_labels[isig])
2917+
for osys, osig in dropped_outputs:
2918+
outlist.append((osys, osig))
2919+
outputs.append(newsys.syslist[osys].output_labels[osig])
2920+
2921+
# Rebuild the system with new inputs/outputs
2922+
newsys = InterconnectedSystem(
2923+
syslist, connections=connections, inplist=inplist,
2924+
outlist=outlist, inputs=inputs, outputs=outputs, states=states,
2925+
params=params, dt=dt, name=name, warn_duplicate=warn_duplicate)
2926+
28932927
# check for implicitly dropped signals
28942928
if check_unused:
28952929
newsys.check_unused_signals(ignore_inputs, ignore_outputs)

control/statefbk.py

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -603,8 +603,8 @@ def dlqr(*args, **kwargs):
603603
def create_statefbk_iosystem(
604604
sys, gain, integral_action=None, estimator=None, controller_type=None,
605605
xd_labels='xd[{i}]', ud_labels='ud[{i}]', gainsched_indices=None,
606-
gainsched_method='linear', name=None, inputs=None, outputs=None,
607-
states=None, **kwargs):
606+
gainsched_method='linear', control_indices=None, state_indices=None,
607+
name=None, inputs=None, outputs=None, states=None, **kwargs):
608608
"""Create an I/O system using a (full) state feedback controller
609609
610610
This function creates an input/output system that implements a
@@ -714,6 +714,20 @@ def create_statefbk_iosystem(
714714
715715
Other Parameters
716716
----------------
717+
control_indices : list of int or str, optional
718+
Specify the indices of the system inputs that should be determined
719+
by the state feedback controller. If not specified, defaults to
720+
the first `m` system inputs, where `m` is determined by the shape
721+
of the gain matrix, with the remaining inputs remaining as inputs
722+
to the overall closed loop system.
723+
724+
state_indices : list of int or str, optional
725+
Specify the indices of the system (or estimator) outputs that
726+
should be used by the state feedback controller. If not specified,
727+
defaults to the first `n` system/estimator outputs, where `n` is
728+
determined by the shape of the gain matrix, with the remaining
729+
outputs remaining as outputs to the overall closed loop system.
730+
717731
inputs, outputs : str, or list of str, optional
718732
List of strings that name the individual signals of the transformed
719733
system. If not given, the inputs and outputs are the same as the
@@ -740,47 +754,63 @@ def create_statefbk_iosystem(
740754
if kwargs:
741755
raise TypeError("unrecognized keywords: ", str(kwargs))
742756

743-
# See whether we were give an estimator
744-
if estimator is not None:
745-
# Check to make sure the estimator is the right size
746-
if estimator.noutputs != sys.nstates:
747-
raise ControlArgument("Estimator output size must match state")
748-
elif sys.noutputs != sys.nstates:
757+
# Figure out what inputs to the system to use
758+
control_indices = range(sys.ninputs) if control_indices is None \
759+
else list(control_indices)
760+
for i, idx in enumerate(control_indices):
761+
if isinstance(idx, str):
762+
control_indices[i] = sys.input_labels.index(control_indices[i])
763+
sys_ninputs = len(control_indices)
764+
765+
# Decide what system is going to pass the states to the controller
766+
if estimator is None:
767+
estimator = sys
768+
769+
# Figure out what outputs (states) from the system/estimator to use
770+
state_indices = range(sys.nstates) if state_indices is None \
771+
else list(state_indices)
772+
for i, idx in enumerate(state_indices):
773+
if isinstance(idx, str):
774+
state_indices[i] = estimator.state_labels.index(state_indices[i])
775+
sys_nstates = len(state_indices)
776+
777+
# Make sure the system/estimator states are proper dimension
778+
if estimator.noutputs < sys_nstates:
749779
# If no estimator, make sure that the system has all states as outputs
750-
# TODO: check to make sure output map is the identity
751-
raise ControlArgument("System output must be the full state")
752-
else:
780+
raise ControlArgument(
781+
("system" if estimator == sys else "estimator") +
782+
" output must include the full state")
783+
elif estimator == sys:
753784
# Issue a warning if we can't verify state output
754785
if (isinstance(sys, NonlinearIOSystem) and sys.outfcn is not None) or \
755786
(isinstance(sys, StateSpace) and
756-
not (np.all(sys.C == np.eye(sys.nstates)) and np.all(sys.D == 0))):
787+
not (np.all(sys.C[np.ix_(state_indices, state_indices)] ==
788+
np.eye(sys_nstates)) and
789+
np.all(sys.D[state_indices, :] == 0))):
757790
warnings.warn("cannot verify system output is system state")
758791

759-
# Use the system directly instead of an estimator
760-
estimator = sys
761-
762792
# See whether we should implement integral action
763793
nintegrators = 0
764794
if integral_action is not None:
765795
if not isinstance(integral_action, np.ndarray):
766796
raise ControlArgument("Integral action must pass an array")
767-
elif integral_action.shape[1] != sys.nstates:
797+
elif integral_action.shape[1] != sys_nstates:
768798
raise ControlArgument(
769799
"Integral gain size must match system state size")
770800
else:
771801
nintegrators = integral_action.shape[0]
772802
C = integral_action
773803
else:
774804
# Create a C matrix with no outputs, just in case update gets called
775-
C = np.zeros((0, sys.nstates))
805+
C = np.zeros((0, sys_nstates))
776806

777807
# Check to make sure that state feedback has the right shape
778808
if isinstance(gain, np.ndarray):
779809
K = gain
780-
if K.shape != (sys.ninputs, estimator.noutputs + nintegrators):
810+
if K.shape != (sys_ninputs, estimator.noutputs + nintegrators):
781811
raise ControlArgument(
782-
f'Control gain must be an array of size {sys.ninputs}'
783-
f'x {sys.nstates}' +
812+
f'control gain must be an array of size {sys_ninputs}'
813+
f' x {sys_nstates}' +
784814
(f'+{nintegrators}' if nintegrators > 0 else ''))
785815
gainsched = False
786816

@@ -811,24 +841,24 @@ def create_statefbk_iosystem(
811841
# Figure out the labels to use
812842
if isinstance(xd_labels, str):
813843
# Generate the list of labels using the argument as a format string
814-
xd_labels = [xd_labels.format(i=i) for i in range(sys.nstates)]
844+
xd_labels = [xd_labels.format(i=i) for i in range(sys_nstates)]
815845

816846
if isinstance(ud_labels, str):
817847
# Generate the list of labels using the argument as a format string
818-
ud_labels = [ud_labels.format(i=i) for i in range(sys.ninputs)]
848+
ud_labels = [ud_labels.format(i=i) for i in range(sys_ninputs)]
819849

820850
# Create the signal and system names
821851
if inputs is None:
822852
inputs = xd_labels + ud_labels + estimator.output_labels
823853
if outputs is None:
824-
outputs = list(sys.input_index.keys())
854+
outputs = [sys.input_labels[i] for i in control_indices]
825855
if states is None:
826856
states = nintegrators
827857

828858
# Process gainscheduling variables, if present
829859
if gainsched:
830860
# Create a copy of the scheduling variable indices (default = xd)
831-
gainsched_indices = range(sys.nstates) if gainsched_indices is None \
861+
gainsched_indices = range(sys_nstates) if gainsched_indices is None \
832862
else list(gainsched_indices)
833863

834864
# If points is a 1D list, convert to 2D
@@ -877,8 +907,8 @@ def _compute_gain(mu):
877907
# Create an I/O system for the state feedback gains
878908
def _control_update(t, states, inputs, params):
879909
# Split input into desired state, nominal input, and current state
880-
xd_vec = inputs[0:sys.nstates]
881-
x_vec = inputs[-estimator.nstates:]
910+
xd_vec = inputs[0:sys_nstates]
911+
x_vec = inputs[-sys_nstates:]
882912

883913
# Compute the integral error in the xy coordinates
884914
return C @ (x_vec - xd_vec)
@@ -891,14 +921,14 @@ def _control_output(t, states, inputs, params):
891921
K_ = params.get('K')
892922

893923
# Split input into desired state, nominal input, and current state
894-
xd_vec = inputs[0:sys.nstates]
895-
ud_vec = inputs[sys.nstates:sys.nstates + sys.ninputs]
896-
x_vec = inputs[-sys.nstates:]
924+
xd_vec = inputs[0:sys_nstates]
925+
ud_vec = inputs[sys_nstates:sys_nstates + sys_ninputs]
926+
x_vec = inputs[-sys_nstates:]
897927

898928
# Compute the control law
899-
u = ud_vec - K_[:, 0:sys.nstates] @ (x_vec - xd_vec)
929+
u = ud_vec - K_[:, 0:sys_nstates] @ (x_vec - xd_vec)
900930
if nintegrators > 0:
901-
u -= K_[:, sys.nstates:] @ states
931+
u -= K_[:, sys_nstates:] @ states
902932

903933
return u
904934

@@ -915,10 +945,10 @@ def _control_output(t, states, inputs, params):
915945
else:
916946
# Discrete time: summer
917947
A_lqr = np.eye(C.shape[0])
918-
B_lqr = np.hstack([-C, np.zeros((C.shape[0], sys.ninputs)), C])
919-
C_lqr = -K[:, sys.nstates:]
948+
B_lqr = np.hstack([-C, np.zeros((C.shape[0], sys_ninputs)), C])
949+
C_lqr = -K[:, sys_nstates:]
920950
D_lqr = np.hstack([
921-
K[:, 0:sys.nstates], np.eye(sys.ninputs), -K[:, 0:sys.nstates]
951+
K[:, 0:sys_nstates], np.eye(sys_ninputs), -K[:, 0:sys_nstates]
922952
])
923953

924954
ctrl = ss(
@@ -929,12 +959,16 @@ def _control_output(t, states, inputs, params):
929959
raise ControlArgument(f"unknown controller_type '{controller_type}'")
930960

931961
# Define the closed loop system
962+
inplist=inputs[:-sys.nstates]
963+
input_labels=inputs[:-sys.nstates]
964+
outlist=sys.output_labels + [sys.input_labels[i] for i in control_indices]
965+
output_labels=sys.output_labels + \
966+
[sys.input_labels[i] for i in control_indices]
932967
closed = interconnect(
933968
[sys, ctrl] if estimator == sys else [sys, ctrl, estimator],
934-
name=sys.name + "_" + ctrl.name,
935-
inplist=inputs[:-sys.nstates], inputs=inputs[:-sys.nstates],
936-
outlist=sys.output_labels + sys.input_labels,
937-
outputs=sys.output_labels + sys.input_labels
969+
name=sys.name + "_" + ctrl.name, add_unused=True,
970+
inplist=inplist, inputs=input_labels,
971+
outlist=outlist, outputs=output_labels
938972
)
939973
return ctrl, closed
940974

control/stochsys.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,7 @@ def white_noise(T, Q, dt=0):
564564
# Return a linear combination of the noise sources
565565
return sp.linalg.sqrtm(Q) @ W
566566

567+
567568
def correlation(T, X, Y=None, squeeze=True):
568569
"""Compute the correlation of time signals.
569570

0 commit comments

Comments
 (0)