Skip to content

Commit ebedf19

Browse files
committed
regularize *_labels processing across create_*_iosystem
1 parent 1c1ce0c commit ebedf19

9 files changed

Lines changed: 175 additions & 83 deletions

File tree

control/config.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import collections
1212
import warnings
13+
from .exception import ControlArgument
1314

1415
__all__ = ['defaults', 'set_defaults', 'reset_defaults',
1516
'use_matlab_defaults', 'use_fbs_defaults',
@@ -310,3 +311,25 @@ def use_legacy_defaults(version):
310311
set_defaults('nyquist', mirror_style='-')
311312

312313
return (major, minor, patch)
314+
315+
316+
#
317+
# Utility function for processing legacy keywords
318+
#
319+
# Use this function to handle a legacy keyword that has been renamed. This
320+
# function pops the old keyword off of the kwargs dictionary and issues a
321+
# warning. if both the old and new keyword are present, a ControlArgument
322+
# exception is raised.
323+
#
324+
def _process_legacy_keyword(kwargs, oldkey, newkey, newval):
325+
if kwargs.get(oldkey) is not None:
326+
warnings.warn(
327+
f"keyworld '{oldkey}' is deprecated; use '{newkey}'",
328+
DeprecationWarning)
329+
if newval is not None:
330+
raise ControlArgument(
331+
f"duplicate keywords '{oldkey}' and '{newkey}'")
332+
else:
333+
return kwargs.pop(oldkey)
334+
else:
335+
return newval

control/iosys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class for a set of subclasses that are used to implement specific
8282
System name (used for specifying signals). If unspecified, a generic
8383
name <sys[id]> is generated with a unique integer id.
8484
params : dict, optional
85-
Parameter values for the systems. Passed to the evaluation functions
85+
Parameter values for the system. Passed to the evaluation functions
8686
for the system as default values, overriding internal defaults.
8787
8888
Attributes

control/namedio.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,3 +666,21 @@ def _process_control_disturbance_indices(
666666
ctrl_idx = [i for i in range(sys.ninputs) if i not in dist_idx]
667667

668668
return ctrl_idx, dist_idx
669+
670+
671+
# Process labels
672+
def _process_labels(labels, name, default):
673+
if isinstance(labels, str):
674+
labels = [labels.format(i=i) for i in range(len(default))]
675+
676+
if labels is None:
677+
labels = default
678+
elif isinstance(labels, list):
679+
if len(labels) != len(default):
680+
raise ValueError(
681+
f"incorrect length of {name}_labels: {len(labels)}"
682+
f" instead of {len(default)}")
683+
else:
684+
raise ValueError(f"{name}_labels should be a string or a list")
685+
686+
return labels

control/optimal.py

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818

1919
from . import config
2020
from .exception import ControlNotImplemented
21-
from .namedio import _process_indices, _process_control_disturbance_indices
21+
from .namedio import _process_indices, _process_labels, \
22+
_process_control_disturbance_indices
23+
2224

2325
# Define module default parameter values
2426
_optimal_trajectory_methods = {'shooting', 'collocation'}
@@ -859,11 +861,11 @@ def _output(t, x, u, params={}):
859861
return res.inputs[:, 0]
860862

861863
# Define signal names, if they are not already given
862-
if not kwargs.get('inputs'):
864+
if kwargs.get('inputs') is None:
863865
kwargs['inputs'] = self.system.state_labels
864-
if not kwargs.get('outputs'):
866+
if kwargs.get('outputs') is None:
865867
kwargs['outputs'] = self.system.input_labels
866-
if not kwargs.get('states'):
868+
if kwargs.get('states') is None:
867869
kwargs['states'] = self.system.ninputs * \
868870
(self.timepts.size if self.basis is None else self.basis.N)
869871

@@ -1125,6 +1127,15 @@ def create_mpc_iosystem(
11251127
returning the current input to be applied that minimizes the cost
11261128
function while satisfying the constraints.
11271129
1130+
Other Parameters
1131+
----------------
1132+
inputs, outputs, states : int or list of str, optional
1133+
Set the names of the inputs, outputs, and states, as described in
1134+
:func:`~control.InputOutputSystem`.
1135+
name : string, optional
1136+
System name (used for specifying signals). If unspecified, a generic
1137+
name <sys[id]> is generated with a unique integer id.
1138+
11281139
Notes
11291140
-----
11301141
Additional keyword parameters can be used to fine tune the behavior of
@@ -1676,9 +1687,9 @@ def compute_estimate(
16761687
# xhat, u, v, y for all previous time points. When the system update
16771688
# function is called,
16781689
#
1679-
# TODO: change output_labels to output_fmtstr and use output instead
1680-
#
1681-
def create_mhe_iosystem(self, output_labels=None, **kwargs):
1690+
def create_mhe_iosystem(
1691+
self, estimate_labels=None, measurement_labels=None,
1692+
control_labels=None, inputs=None, outputs=None, **kwargs):
16821693
"""Create an I/O system implementing an MPC controller
16831694
16841695
This function creates an input/output system that implements a
@@ -1688,12 +1699,24 @@ def create_mhe_iosystem(self, output_labels=None, **kwargs):
16881699
16891700
Parameters
16901701
----------
1691-
output_labels : str, optional
1692-
Set the name of the estimator outputs (state estimate). If a
1693-
single string is specified, it should be a format string using
1694-
the variable `i` as an index. Otherwise, a list of strings
1695-
matching the size of the system state should be used. Default
1696-
is "xhat[{i}]".
1702+
estimate_labels : str or list of str, optional
1703+
Set the name of the signals to use for the estimated state
1704+
(estimator outputs). If a single string is specified, it
1705+
should be a format string using the variable ``i`` as an index.
1706+
Otherwise, a list of strings matching the size of the estimated
1707+
state should be used. Default is "xhat[{i}]". These settings
1708+
can also be overriden using the `outputs` keyword.
1709+
measurement_labels, control_labels : str or list of str, optional
1710+
Set the name of the measurement and control signal names
1711+
(estimator inputs). If a single string is specified, it should
1712+
be a format string using the variable ``i`` as an index.
1713+
Otherwise, a list of strings matching the size of the system
1714+
inputs and outputs should be used. Default is the signal names
1715+
for the system outputs and control inputs. These settings can
1716+
also be overriden using the `inputs` keyword.
1717+
**kwargs, optional
1718+
Additional keyword arguments to set system, input, and output
1719+
signal names; see :func:`~control.InputOutputSystem`.
16971720
16981721
Returns
16991722
-------
@@ -1721,20 +1744,24 @@ def create_mhe_iosystem(self, output_labels=None, **kwargs):
17211744
_process_control_disturbance_indices(
17221745
self.system, self.control_indices, self.disturbance_indices)
17231746

1724-
# Figure out the labels to use
1725-
# TODO: allow overwrite via kwargs + change parameter name
1726-
if isinstance(output_labels, str):
1727-
# Generate labels using the argument as a format string
1728-
output_labels = [output_labels.format(i=i)
1729-
for i in range(self.system.nstates)]
1747+
# Figure out the signal labels to use
1748+
estimate_labels = _process_labels(
1749+
estimate_labels, 'estimate',
1750+
[f'xhat[{i}]' for i in range(self.system.nstates)])
1751+
outputs = estimate_labels if outputs is None else outputs
17301752

1731-
# TODO: allow overwrite via kwargs
1732-
sensor_labels = [self.system.output_labels [i]
1733-
for i in range(self.system.noutputs)]
1734-
input_labels = [self.system.input_labels[i] for i in self.ctrl_idx]
1753+
measurement_labels = _process_labels(
1754+
measurement_labels, 'measurement', self.system.output_labels)
1755+
control_labels = _process_labels(
1756+
control_labels, 'control',
1757+
[self.system.input_labels[i] for i in self.ctrl_idx])
1758+
inputs = measurement_labels + control_labels if inputs is None \
1759+
else inputs
17351760

17361761
nstates = (self.system.nstates + self.system.ninputs
17371762
+ self.system.noutputs) * self.timepts.size
1763+
if kwargs.get('states'):
1764+
raise ValueError("user-specified state signal names not allowed")
17381765

17391766
# Utility function to extract elements from MHE state vector
17401767
def _xvec_next(xvec, off, size):
@@ -1781,9 +1808,8 @@ def _mhe_output(t, xvec, uvec, params={}):
17811808
return self.system._rhs(t, xhat[:, -1], u_v[:, -1])
17821809

17831810
return ct.NonlinearIOSystem(
1784-
_mhe_update, _mhe_output, states=nstates,
1785-
inputs=sensor_labels + input_labels,
1786-
outputs=output_labels, dt=self.system.dt, **kwargs)
1811+
_mhe_update, _mhe_output, dt=self.system.dt,
1812+
states=nstates, inputs=inputs, outputs=outputs, **kwargs)
17871813

17881814

17891815
# Optimal estimation result

control/statefbk.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@
4848
from .mateqn import care, dare, _check_shape
4949
from .statesp import StateSpace, _ssmatrix, _convert_to_statespace
5050
from .lti import LTI
51-
from .namedio import isdtime, isctime, _process_indices
51+
from .namedio import isdtime, isctime, _process_indices, _process_labels
5252
from .iosys import InputOutputSystem, NonlinearIOSystem, LinearIOSystem, \
5353
interconnect, ss
5454
from .exception import ControlSlycot, ControlArgument, ControlDimension, \
5555
ControlNotImplemented
56+
from .config import _process_legacy_keyword
5657

5758
# Make sure we have access to the right slycot routines
5859
try:
@@ -602,7 +603,7 @@ def dlqr(*args, **kwargs):
602603
# Function to create an I/O sytems representing a state feedback controller
603604
def create_statefbk_iosystem(
604605
sys, gain, integral_action=None, estimator=None, controller_type=None,
605-
xd_labels='xd[{i}]', ud_labels='ud[{i}]', gainsched_indices=None,
606+
xd_labels=None, ud_labels=None, gainsched_indices=None,
606607
gainsched_method='linear', control_indices=None, state_indices=None,
607608
name=None, inputs=None, outputs=None, states=None, **kwargs):
608609
"""Create an I/O system using a (full) state feedback controller
@@ -747,14 +748,8 @@ def create_statefbk_iosystem(
747748
raise ControlArgument("Input system must be I/O system")
748749

749750
# Process (legacy) keywords
750-
if kwargs.get('type') is not None:
751-
warnings.warn(
752-
"keyword 'type' is deprecated; use 'controller_type'",
753-
DeprecationWarning)
754-
if controller_type is not None:
755-
raise ControlArgument(
756-
"duplicate keywords 'type` and 'controller_type'")
757-
controller_type = kwargs.pop('type')
751+
controller_type = _process_legacy_keyword(
752+
kwargs, 'type', 'controller_type', controller_type)
758753
if kwargs:
759754
raise TypeError("unrecognized keywords: ", str(kwargs))
760755

@@ -837,13 +832,10 @@ def create_statefbk_iosystem(
837832
raise ControlArgument(f"unknown controller_type '{controller_type}'")
838833

839834
# Figure out the labels to use
840-
if isinstance(xd_labels, str):
841-
# Generate the list of labels using the argument as a format string
842-
xd_labels = [xd_labels.format(i=i) for i in range(sys_nstates)]
843-
844-
if isinstance(ud_labels, str):
845-
# Generate the list of labels using the argument as a format string
846-
ud_labels = [ud_labels.format(i=i) for i in range(sys_ninputs)]
835+
xd_labels = _process_labels(
836+
xd_labels, 'xd', ['xd[{i}]'.format(i=i) for i in range(sys_nstates)])
837+
ud_labels = _process_labels(
838+
ud_labels, 'ud', ['ud[{i}]'.format(i=i) for i in range(sys_ninputs)])
847839

848840
# Create the signal and system names
849841
if inputs is None:

0 commit comments

Comments
 (0)