Skip to content

Commit 5fc5bfc

Browse files
committed
first pass at uniform argument names for time responses (+ aliases)
1 parent cf77f99 commit 5fc5bfc

24 files changed

+538
-152
lines changed

control/config.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ def use_legacy_defaults(version):
384384
def _process_legacy_keyword(kwargs, oldkey, newkey, newval, warn_oldkey=True):
385385
"""Utility function for processing legacy keywords.
386386
387+
.. deprecated:: 0.10.2
388+
Replace with `_process_param` or `_process_kwargs`.
389+
387390
Use this function to handle a legacy keyword that has been renamed.
388391
This function pops the old keyword off of the kwargs dictionary and
389392
issues a warning. If both the old and new keyword are present, a
@@ -412,6 +415,9 @@ def _process_legacy_keyword(kwargs, oldkey, newkey, newval, warn_oldkey=True):
412415
Value of the (new) keyword.
413416
414417
"""
418+
warnings.warn(
419+
"replace `_process_legacy_keyword` with `_process_param` "
420+
"or `_process_kwargs`", PendingDeprecationWarning)
415421
if oldkey in kwargs:
416422
if warn_oldkey:
417423
warnings.warn(
@@ -424,3 +430,144 @@ def _process_legacy_keyword(kwargs, oldkey, newkey, newval, warn_oldkey=True):
424430
return kwargs.pop(oldkey)
425431
else:
426432
return newval
433+
434+
435+
def _process_param(name, defval, kwargs, alias_mapping, sigval=None):
436+
"""Process named parameter, checking aliases and legacy usage.
437+
438+
Helper function to process function arguments by mapping aliases to
439+
either their default keywords or to a named argument. The alias
440+
mapping is a dictionary that returns a tuple consisting of valid
441+
aliases and legacy aliases::
442+
443+
alias_mapping = {
444+
'argument_name_1', (['alias', ...], ['legacy', ...]),
445+
...}
446+
447+
If `param` is a named keyword in the function signature with default
448+
value `defval`, a typical calling sequence at the start of a function
449+
is::
450+
451+
param = _process_param('param', defval, kwargs, function_aliases)
452+
453+
If `param` is a variable keyword argument (in `kwargs`), `defval` can
454+
be pssed as either None or the default value to use if `param` is not
455+
present in `kwargs`.
456+
457+
Parameters
458+
----------
459+
name : str
460+
Name of the parameter to be checked.
461+
defval : object or dict
462+
Default value for the parameter.
463+
kwargs : dict
464+
Dictionary of varaible keyword arguments.
465+
alias_mapping : dict
466+
Dictionary providing aliases and legacy names.
467+
sigval : object, optional
468+
Default value specified in the function signature (default = None).
469+
If specified, an error will be generated if `defval` is different
470+
than `sigval` and an alias or legacy keyword is given.
471+
472+
Returns
473+
-------
474+
newval : object
475+
New value of the named parameter.
476+
477+
Raises
478+
------
479+
TypeError
480+
If multiple keyword aliased are used for the same parameter.
481+
482+
Warns
483+
-----
484+
PendingDeprecationWarning
485+
If legacy name is used to set the value for the variable.
486+
487+
"""
488+
# Check to see if the parameter is in the keyword list
489+
if name in kwargs:
490+
if defval != sigval:
491+
raise TypeError(f"multiple values for parameter {name}")
492+
newval = kwargs.pop(name)
493+
else:
494+
newval = defval
495+
496+
# Get the list of aliases and legacy names
497+
aliases, legacy = alias_mapping[name]
498+
499+
for kw in legacy:
500+
if kw in kwargs:
501+
warnings.warn(
502+
f"alias `{kw}` is legacy name; use `{name}` instead",
503+
PendingDeprecationWarning)
504+
kwval = kwargs.pop(kw)
505+
if newval != defval and kwval != newval:
506+
raise TypeError(
507+
f"multiple values for parameter `{name}` (via {kw})")
508+
newval = kwval
509+
510+
for kw in aliases:
511+
if kw in kwargs:
512+
kwval = kwargs.pop(kw)
513+
if newval != defval and kwval != newval:
514+
raise TypeError(
515+
f"multiple values for parameter `{name}` (via {kw})")
516+
newval = kwval
517+
518+
return newval
519+
520+
521+
def _process_kwargs(kwargs, alias_mapping):
522+
"""Process aliases and legacy keywords.
523+
524+
Helper function to process function arguments by mapping aliases to
525+
their default keywords. The alias mapping is a dictionary that returns
526+
a tuple consisting of valid aliases and legacy aliases::
527+
528+
alias_mapping = {
529+
'argument_name_1', (['alias', ...], ['legacy', ...]),
530+
...}
531+
532+
If an alias is present in the dictionary of keywords, it will be used
533+
to set the value of the argument. If a legacy keyword is used, a
534+
warning is issued.
535+
536+
Parameters
537+
----------
538+
kwargs : dict
539+
Dictionary of variable keyword arguments.
540+
alias_mapping : dict
541+
Dictionary providing aliases and legacy names.
542+
543+
Raises
544+
------
545+
TypeError
546+
If multiple keyword aliased are used for the same parameter.
547+
548+
Warns
549+
-----
550+
PendingDeprecationWarning
551+
If legacy name is used to set the value for the variable.
552+
553+
"""
554+
for name in alias_mapping or []:
555+
aliases, legacy = alias_mapping[name]
556+
newval = None
557+
558+
for kw in legacy:
559+
if kw in kwargs:
560+
warnings.warn(
561+
f"alias `{kw}` is legacy name; use `{name}` instead",
562+
PendingDeprecationWarning)
563+
if name in kwargs:
564+
raise TypeError(
565+
f"multiple values for parameter `{name}` (via {kw})")
566+
kwargs[name] = kwargs.pop(kw)
567+
568+
for kw in aliases:
569+
if kw in kwargs:
570+
if name in kwargs:
571+
raise TypeError(
572+
f"multiple values for parameter `{name}` (via {kw})")
573+
kwargs[name] = kwargs.pop(kw)

control/nlsys.py

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@
2222
import scipy as sp
2323

2424
from . import config
25+
from .config import _process_param, _process_kwargs
2526
from .iosys import InputOutputSystem, _parse_spec, _process_iosys_keywords, \
2627
common_timebase, iosys_repr, isctime, isdtime
2728
from .timeresp import TimeResponseData, TimeResponseList, \
28-
_check_convert_array, _process_time_response
29+
_check_convert_array, _process_time_response, _timeresp_aliases
2930

3031
__all__ = ['NonlinearIOSystem', 'InterconnectedSystem', 'nlsys',
3132
'input_output_response', 'find_eqpt', 'linearize',
@@ -1471,9 +1472,9 @@ def nlsys(updfcn, outfcn=None, **kwargs):
14711472

14721473

14731474
def input_output_response(
1474-
sys, T, U=0., X0=0, params=None, ignore_errors=False,
1475-
transpose=False, return_x=False, squeeze=None,
1476-
solve_ivp_kwargs=None, t_eval='T', **kwargs):
1475+
sys, timepts=None, inputs=0., initial_state=0., params=None,
1476+
ignore_errors=False, transpose=False, return_states=False,
1477+
squeeze=None, solve_ivp_kwargs=None, evaluation_times='T', **kwargs):
14771478
"""Compute the output response of a system to a given input.
14781479
14791480
Simulate a dynamical system with a given input and return its output
@@ -1483,22 +1484,23 @@ def input_output_response(
14831484
----------
14841485
sys : `NonlinearIOSystem` or list of `NonlinearIOSystem`
14851486
I/O system(s) for which input/output response is simulated.
1486-
T : array_like
1487+
timepts (or T) : array_like
14871488
Time steps at which the input is defined; values must be evenly spaced.
1488-
U : array_like, list, or number, optional
1489-
Input array giving input at each time `T` (default = 0). If a list
1490-
is specified, each element in the list will be treated as a portion
1491-
of the input and broadcast (if necessary) to match the time vector.
1492-
X0 : array_like, list, or number, optional
1489+
inputs (or U) : array_like, list, or number, optional
1490+
Input array giving input at each time in `timepts` (default =
1491+
0). If a list is specified, each element in the list will be
1492+
treated as a portion of the input and broadcast (if necessary) to
1493+
match the time vector.
1494+
initial_state (or X0) : array_like, list, or number, optional
14931495
Initial condition (default = 0). If a list is given, each element
14941496
in the list will be flattened and stacked into the initial
14951497
condition. If a smaller number of elements are given that the
14961498
number of states in the system, the initial condition will be padded
14971499
with zeros.
1498-
t_eval : array-list, optional
1500+
evaluation_times (or t_eval) : array-list, optional
14991501
List of times at which the time response should be computed.
1500-
Defaults to `T`.
1501-
return_x : bool, optional
1502+
Defaults to `timepts`.
1503+
return_states (or return_x) : bool, optional
15021504
If True, return the state vector when assigning to a tuple. See
15031505
`forced_response` for more details. If True, return the values of
15041506
the state at each time Default is False.
@@ -1523,7 +1525,7 @@ def input_output_response(
15231525
method. See `TimeResponseData` for more detailed information.
15241526
response.time : array
15251527
Time values of the output.
1526-
response.output : array
1528+
response.outputs : array
15271529
Response of the system. If the system is SISO and `squeeze` is not
15281530
True, the array is 1D (indexed by time). If the system is not SISO
15291531
or `squeeze` is False, the array is 2D (indexed by output and time).
@@ -1581,6 +1583,18 @@ def input_output_response(
15811583
#
15821584
# Process keyword arguments
15831585
#
1586+
_process_kwargs(kwargs, _timeresp_aliases)
1587+
T = _process_param('timepts', timepts, kwargs, _timeresp_aliases)
1588+
U = _process_param('inputs', inputs, kwargs, _timeresp_aliases, sigval=0.)
1589+
X0 = _process_param(
1590+
'initial_state', initial_state, kwargs, _timeresp_aliases, sigval=0.)
1591+
return_x = _process_param(
1592+
'return_states', return_states, kwargs, _timeresp_aliases,
1593+
sigval=False)
1594+
# TODO: replace default value of evaluation_times with None?
1595+
t_eval = _process_param(
1596+
'evaluation_times', evaluation_times, kwargs, _timeresp_aliases,
1597+
sigval='T')
15841598

15851599
# Figure out the method to be used
15861600
solve_ivp_kwargs = solve_ivp_kwargs.copy() if solve_ivp_kwargs else {}
@@ -1605,9 +1619,10 @@ def input_output_response(
16051619
sysdata, responses = sys, []
16061620
for sys in sysdata:
16071621
responses.append(input_output_response(
1608-
sys, T, U=U, X0=X0, params=params, transpose=transpose,
1609-
return_x=return_x, squeeze=squeeze, t_eval=t_eval,
1610-
solve_ivp_kwargs=solve_ivp_kwargs, **kwargs))
1622+
sys, timepts=T, inputs=U, initial_state=X0, params=params,
1623+
transpose=transpose, return_states=return_x, squeeze=squeeze,
1624+
evaluation_times=t_eval, solve_ivp_kwargs=solve_ivp_kwargs,
1625+
**kwargs))
16111626
return TimeResponseList(responses)
16121627

16131628
# Sanity checking on the input
@@ -1894,8 +1909,9 @@ def __len__(self):
18941909

18951910

18961911
def find_operating_point(
1897-
sys, x0, u0=None, y0=None, t=0, params=None, iu=None, iy=None,
1898-
ix=None, idx=None, dx0=None, root_method=None, root_kwargs=None,
1912+
sys, initial_state=0., inputs=None, outputs=None, t=0, params=None,
1913+
input_indices=None, output_indices=None, state_indices=None,
1914+
deriv_indices=None, derivs=None, root_method=None, root_kwargs=None,
18991915
return_outputs=None, return_result=None, **kwargs):
19001916
"""Find an operating point for an input/output system.
19011917
@@ -1929,36 +1945,36 @@ def find_operating_point(
19291945
----------
19301946
sys : `NonlinearIOSystem`
19311947
I/O system for which the operating point is sought.
1932-
x0 : list of initial state values
1948+
initial_state (or x0) : list of initial state values
19331949
Initial guess for the value of the state near the operating point.
1934-
u0 : list of input values, optional
1950+
inputs (or u0) : list of input values, optional
19351951
If `y0` is not specified, sets the value of the input. If `y0` is
19361952
given, provides an initial guess for the value of the input. Can
19371953
be omitted if the system does not have any inputs.
1938-
y0 : list of output values, optional
1954+
outputs (or y0) : list of output values, optional
19391955
If specified, sets the desired values of the outputs at the
19401956
operating point.
19411957
t : float, optional
19421958
Evaluation time, for time-varying systems.
19431959
params : dict, optional
19441960
Parameter values for the system. Passed to the evaluation functions
19451961
for the system as default values, overriding internal defaults.
1946-
iu : list of input indices, optional
1962+
input_indices (or iu) : list of input indices, optional
19471963
If specified, only the inputs with the given indices will be fixed at
19481964
the specified values in solving for an operating point. All other
19491965
inputs will be varied. Input indices can be listed in any order.
1950-
iy : list of output indices, optional
1966+
output_indices (or iy) : list of output indices, optional
19511967
If specified, only the outputs with the given indices will be fixed
19521968
at the specified values in solving for an operating point. All other
19531969
outputs will be varied. Output indices can be listed in any order.
1954-
ix : list of state indices, optional
1970+
state_indices (or ix) : list of state indices, optional
19551971
If specified, states with the given indices will be fixed at the
19561972
specified values in solving for an operating point. All other
19571973
states will be varied. State indices can be listed in any order.
1958-
dx0 : list of update values, optional
1974+
derivs (or dx0) : list of update values, optional
19591975
If specified, the value of update map must match the listed value
19601976
instead of the default value for an equilibrium point.
1961-
idx : list of state indices, optional
1977+
deriv_indices (or idx) : list of state indices, optional
19621978
If specified, state updates with the given indices will have their
19631979
update maps fixed at the values given in `dx0`. All other update
19641980
values will be ignored in solving for an operating point. State
@@ -2014,8 +2030,28 @@ def find_operating_point(
20142030
from scipy.optimize import root
20152031

20162032
# Process keyword arguments
2017-
return_outputs = config._process_legacy_keyword(
2018-
kwargs, 'return_y', 'return_outputs', return_outputs)
2033+
aliases = {
2034+
'initial_state': (['x0', 'X0'], []),
2035+
'inputs': (['u0'], []),
2036+
'outputs': (['y0'], []),
2037+
'derivs': (['dx0'], []),
2038+
'input_indices': (['iu'], []),
2039+
'output_indices': (['iy'], []),
2040+
'state_indices': (['ix'], []),
2041+
'deriv_indices': (['idx'], []),
2042+
'return_outputs': ([], ['return_y']),
2043+
}
2044+
_process_kwargs(kwargs, aliases)
2045+
x0 = _process_param('initial_state', initial_state, kwargs, aliases)
2046+
u0 = _process_param('inputs', inputs, kwargs, aliases)
2047+
y0 = _process_param('outputs', outputs, kwargs, aliases)
2048+
dx0 = _process_param('derivs', derivs, kwargs, aliases)
2049+
iu = _process_param('input_indices', input_indices, kwargs, aliases)
2050+
iy = _process_param('output_indices', output_indices, kwargs, aliases)
2051+
ix = _process_param('state_indices', state_indices, kwargs, aliases)
2052+
idx = _process_param('deriv_indices', deriv_indices, kwargs, aliases)
2053+
return_outputs = _process_param(
2054+
'return_outputs', return_outputs, kwargs, aliases)
20192055
if kwargs:
20202056
raise TypeError("unrecognized keyword(s): " + str(kwargs))
20212057

@@ -2025,9 +2061,9 @@ def find_operating_point(
20252061
root_kwargs['method'] = root_method
20262062

20272063
# Figure out the number of states, inputs, and outputs
2028-
x0, nstates = _process_vector_argument(x0, "x0", sys.nstates)
2029-
u0, ninputs = _process_vector_argument(u0, "u0", sys.ninputs)
2030-
y0, noutputs = _process_vector_argument(y0, "y0", sys.noutputs)
2064+
x0, nstates = _process_vector_argument(x0, "initial_states", sys.nstates)
2065+
u0, ninputs = _process_vector_argument(u0, "inputs", sys.ninputs)
2066+
y0, noutputs = _process_vector_argument(y0, "outputs", sys.noutputs)
20312067

20322068
# Make sure the input arguments match the sizes of the system
20332069
if len(x0) != nstates or \

0 commit comments

Comments
 (0)