Skip to content

Commit 9494092

Browse files
committed
rename obc to optimal, new examples/unit tests
1 parent ea2884d commit 9494092

10 files changed

Lines changed: 309 additions & 186 deletions

File tree

control/config.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def reset_defaults():
6767
defaults.update(_iosys_defaults)
6868

6969

70-
def _get_param(module, param, argval=None, defval=None, pop=False):
70+
def _get_param(module, param, argval=None, defval=None, pop=False, last=False):
7171
"""Return the default value for a configuration option.
7272
7373
The _get_param() function is a utility function used to get the value of a
@@ -91,11 +91,13 @@ def _get_param(module, param, argval=None, defval=None, pop=False):
9191
`config.defaults` dictionary. If a dictionary is provided, then
9292
`module.param` is used to determine the default value. Defaults to
9393
None.
94-
pop : bool
94+
pop : bool, optional
9595
If True and if argval is a dict, then pop the remove the parameter
9696
entry from the argval dict after retreiving it. This allows the use
9797
of a keyword argument list to be passed through to other functions
9898
internal to the function being called.
99+
last : bool, optional
100+
If True, check to make sure dictionary is empy after processing.
99101
100102
"""
101103

@@ -108,7 +110,10 @@ def _get_param(module, param, argval=None, defval=None, pop=False):
108110

109111
# If we were passed a dict for the argval, get the param value from there
110112
if isinstance(argval, dict):
111-
argval = argval.pop(param, None) if pop else argval.get(param, None)
113+
val = argval.pop(param, None) if pop else argval.get(param, None)
114+
if last and argval:
115+
raise TypeError("unrecognized keywords: " + str(argval))
116+
argval = val
112117

113118
# If we were passed a dict for the defval, get the param value from there
114119
if isinstance(defval, dict):

control/obc.py renamed to control/optimal.py

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
# obc.py - optimization based control module
1+
# optimal.py - optimization based control module
22
#
33
# RMM, 11 Feb 2021
44
#
55

6-
"""The :mod:`~control.obc` module provides support for optimization-based
6+
"""The :mod:`~control.optimal` module provides support for optimization-based
77
controllers for nonlinear systems with state and input constraints.
88
99
"""
@@ -249,17 +249,26 @@ def _cost_function(self, inputs):
249249

250250
# Trajectory cost
251251
# TODO: vectorize
252-
cost = 0
253-
for i, t in enumerate(self.time_vector):
254-
if ct.isctime(self.system):
252+
if ct.isctime(self.system):
253+
# Evaluate the costs
254+
costs = [self.integral_cost(states[:, i], inputs[:, i]) for
255+
i in range(self.time_vector.size)]
256+
257+
# Compute the time intervals
258+
dt = np.diff(self.time_vector)
259+
260+
# Integrate the cost
261+
cost = 0
262+
for i in range(self.time_vector.size-1):
255263
# Approximate the integral using trapezoidal rule
256-
if i > 0:
257-
cost += 0.5 * (
258-
self.integral_cost(states[:, i-1], inputs[:, i-1]) +
259-
self.integral_cost(states[:, i], inputs[:, i])) * (
260-
self.time_vector[i] - self.time_vector[i-1])
261-
else:
262-
cost += self.integral_cost(states[:,i], inputs[:,i])
264+
cost += 0.5 * (costs[i] + costs[i+1]) * dt[i]
265+
266+
else:
267+
# Sum the integral cost over the time (second) indices
268+
# cost += self.integral_cost(states[:,i], inputs[:,i])
269+
cost = sum(map(
270+
self.integral_cost, np.transpose(states),
271+
np.transpose(inputs)))
263272

264273
# Terminal cost
265274
if self.terminal_cost is not None:
@@ -526,8 +535,8 @@ def _update(t, x, u, params={}):
526535
inputs = x.reshape((self.system.ninputs, self.time_vector.size))
527536
self.initial_guess = np.hstack(
528537
[inputs[:,1:], inputs[:,-1:]]).reshape(-1)
529-
result = self.compute_trajectory(u)
530-
return result.inputs.reshape(-1)
538+
res = self.compute_trajectory(u, print_summary=False)
539+
return res.inputs.reshape(-1)
531540

532541
def _output(t, x, u, params={}):
533542
inputs = x.reshape((self.system.ninputs, self.time_vector.size))
@@ -541,15 +550,15 @@ def _output(t, x, u, params={}):
541550

542551
# Compute the optimal trajectory from the current state
543552
def compute_trajectory(
544-
self, x, squeeze=None, transpose=None, return_x=None,
545-
print_summary=True):
553+
self, x, squeeze=None, transpose=None, return_states=None,
554+
print_summary=True, **kwargs):
546555
"""Compute the optimal input at state x
547556
548557
Parameters
549558
----------
550559
x : array-like or number, optional
551560
Initial state for the system.
552-
return_x : bool, optional
561+
return_states : bool, optional
553562
If True, return the values of the state at each time (default =
554563
False).
555564
squeeze : bool, optional
@@ -564,17 +573,25 @@ def compute_trajectory(
564573
565574
Returns
566575
-------
567-
time : array
576+
res : OptimalControlResult
577+
Bundle object with the results of the optimal control problem.
578+
res.success: bool
579+
Boolean flag indicating whether the optimization was successful.
580+
res.time : array
568581
Time values of the input.
569-
inputs : array
582+
res.inputs : array
570583
Optimal inputs for the system. If the system is SISO and squeeze
571584
is not True, the array is 1D (indexed by time). If the system is
572585
not SISO or squeeze is False, the array is 2D (indexed by the
573586
output number and time).
574-
states : array
575-
Time evolution of the state vector (if return_x=True).
587+
res.states : array
588+
Time evolution of the state vector (if return_states=True).
576589
577590
"""
591+
# Allow 'return_x` as a synonym for 'return_states'
592+
return_states = ct.config._get_param(
593+
'optimal', 'return_x', kwargs, return_states, pop=True)
594+
578595
# Store the initial state (for use in _constraint_function)
579596
self.x = x
580597

@@ -585,7 +602,7 @@ def compute_trajectory(
585602

586603
# Process and return the results
587604
return OptimalControlResult(
588-
self, res, transpose=transpose, return_x=return_x,
605+
self, res, transpose=transpose, return_states=return_states,
589606
squeeze=squeeze, print_summary=print_summary)
590607

591608
# Compute the current input to apply from the current state (MPC style)
@@ -615,8 +632,8 @@ def compute_mpc(self, x, squeeze=None):
615632
if the optimization failed.
616633
617634
"""
618-
results = self.compute_trajectory(x, squeeze=squeeze)
619-
return inputs[:, 0] if results.success else None
635+
res = self.compute_trajectory(x, squeeze=squeeze)
636+
return inputs[:, 0] if res.success else None
620637

621638

622639
# Optimal control result
@@ -640,8 +657,10 @@ class OptimalControlResult(sp.optimize.OptimizeResult):
640657
641658
"""
642659
def __init__(
643-
self, ocp, res, return_x=False, print_summary=False,
660+
self, ocp, res, return_states=False, print_summary=False,
644661
transpose=None, squeeze=None):
662+
"""Create a OptimalControlResult object"""
663+
645664
# Copy all of the fields we were sent by sp.optimize.minimize()
646665
for key, val in res.items():
647666
setattr(self, key, val)
@@ -663,7 +682,7 @@ def __init__(
663682
if print_summary:
664683
ocp._print_statistics()
665684

666-
if return_x and res.success:
685+
if return_states and res.success:
667686
# Simulate the system if we need the state back
668687
_, _, states = ct.input_output_response(
669688
ocp.system, ocp.time_vector, inputs, ocp.x, return_x=True)
@@ -673,18 +692,18 @@ def __init__(
673692

674693
retval = _process_time_response(
675694
ocp.system, ocp.time_vector, inputs, states,
676-
transpose=transpose, return_x=return_x, squeeze=squeeze)
695+
transpose=transpose, return_x=return_states, squeeze=squeeze)
677696

678697
self.time = retval[0]
679698
self.inputs = retval[1]
680699
self.states = None if states is None else retval[2]
681700

682701

683702
# Compute the input for a nonlinear, (constrained) optimal control problem
684-
def compute_optimal_input(
703+
def solve_ocp(
685704
sys, horizon, X0, cost, constraints=[], terminal_cost=None,
686705
terminal_constraints=[], initial_guess=None, squeeze=None,
687-
transpose=None, return_x=None, log=False, **kwargs):
706+
transpose=None, return_states=None, log=False, **kwargs):
688707

689708
"""Compute the solution to an optimal control problem
690709
@@ -738,7 +757,7 @@ def compute_optimal_input(
738757
log : bool, optional
739758
If `True`, turn on logging messages (using Python logging module).
740759
741-
return_x : bool, optional
760+
return_states : bool, optional
742761
If True, return the values of the state at each time (default = False).
743762
744763
squeeze : bool, optional
@@ -756,15 +775,23 @@ def compute_optimal_input(
756775
757776
Returns
758777
-------
759-
time : array
760-
Time values of the input or `None` if the optimimation fails.
761-
inputs : array
762-
Optimal inputs for the system. If the system is SISO and squeeze is not
763-
True, the array is 1D (indexed by time). If the system is not SISO or
764-
squeeze is False, the array is 2D (indexed by the output number and
765-
time).
766-
states : array
767-
Time evolution of the state vector (if return_x=True).
778+
res : OptimalControlResult
779+
Bundle object with the results of the optimal control problem.
780+
781+
res.success: bool
782+
Boolean flag indicating whether the optimization was successful.
783+
784+
res.time : array
785+
Time values of the input.
786+
787+
res.inputs : array
788+
Optimal inputs for the system. If the system is SISO and squeeze is
789+
not True, the array is 1D (indexed by time). If the system is not
790+
SISO or squeeze is False, the array is 2D (indexed by the output
791+
number and time).
792+
793+
res.states : array
794+
Time evolution of the state vector (if return_states=True).
768795
769796
"""
770797
# Set up the optimal control problem
@@ -775,7 +802,7 @@ def compute_optimal_input(
775802

776803
# Solve for the optimal input from the current state
777804
return ocp.compute_trajectory(
778-
X0, squeeze=squeeze, transpose=None, return_x=None)
805+
X0, squeeze=squeeze, transpose=None, return_states=None)
779806

780807

781808
# Create a model predictive controller for an optimal control problem
@@ -803,7 +830,7 @@ def create_mpc_iosystem(
803830
804831
constraints : list of tuples, optional
805832
List of constraints that should hold at each point in the time vector.
806-
See :func:`~control.obc.compute_optimal_input` for more details.
833+
See :func:`~control.optimal.solve_ocp` for more details.
807834
808835
terminal_cost : callable, optional
809836
Function that returns the terminal cost given the current state

control/tests/config_test.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,14 @@ def test_change_default_dt_static(self):
251251
assert ct.tf(1, 1).dt is None
252252
assert ct.ss(0, 0, 0, 1).dt is None
253253
# TODO: add in test for static gain iosys
254+
255+
def test_get_param_last(self):
256+
"""Test _get_param last keyword"""
257+
kwargs = {'first': 1, 'second': 2}
258+
259+
with pytest.raises(TypeError, match="unrecognized keyword.*second"):
260+
assert ct.config._get_param(
261+
'config', 'first', kwargs, pop=True, last=True) == 1
262+
263+
assert ct.config._get_param(
264+
'config', 'second', kwargs, pop=True, last=True) == 2

0 commit comments

Comments
 (0)