Skip to content

Commit c34779f

Browse files
Merge pull request #18 from pavelacamposp/feature/parameterize-mpc-setpoint
Parameterize controller setpoints
2 parents b9dfbc7 + b3f6779 commit c34779f

9 files changed

+133
-71
lines changed

direct_data_driven_mpc/lti_data_driven_mpc_controller.py

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -493,13 +493,27 @@ def define_optimization_parameters(self) -> None:
493493
Define MPC optimization parameters that are updated at every step
494494
iteration.
495495
496-
This method initializes the past inputs (`u_past_param`) and past
497-
outputs (`y_past_param`) MPC parameters.
496+
This method initializes the input setpoint (`u_s_param`), output
497+
setpoint (`y_s_param`), past inputs (`u_past_param`), and past outputs
498+
(`y_past_param`) MPC parameters.
498499
499-
These parameters are updated at each MPC iteration. Using CVXPY
500-
`Parameter` objects allows efficient updates without the need of
501-
reformulating the MPC problem at every step.
500+
The values of `u_s_param` and `y_s_param` are initialized to `u_s` and
501+
`y_s`.
502+
503+
These parameters are updated at each MPC iteration, except for
504+
`u_s_param` and `y_s_param`, which must be manually updated when
505+
setting new controller setpoint pairs.
506+
507+
Using CVXPY `Parameter` objects allows efficient updates without the
508+
need of reformulating the MPC problem at every step.
502509
"""
510+
# Define input-output setpoint parameters and initialize their values
511+
self.u_s_param = cp.Parameter(self.u_s.shape, name="u_s")
512+
self.u_s_param.value = self.u_s
513+
514+
self.y_s_param = cp.Parameter(self.y_s.shape, name="y_s")
515+
self.y_s_param.value = self.y_s
516+
503517
# u[t-n, t-1]
504518
self.u_past_param = cp.Parameter((self.n * self.m, 1), name="u_past")
505519

@@ -566,7 +580,7 @@ def define_mpc_constraints(self) -> None:
566580
self.define_internal_state_constraints()
567581
)
568582
self.terminal_constraints = (
569-
self.define_terminal_state_constraints(u_s=self.u_s, y_s=self.y_s)
583+
self.define_terminal_state_constraints()
570584
if self.use_terminal_constraints
571585
else []
572586
)
@@ -673,9 +687,7 @@ def define_internal_state_constraints(self) -> list[cp.Constraint]:
673687

674688
return internal_state_constraints
675689

676-
def define_terminal_state_constraints(
677-
self, u_s: np.ndarray, y_s: np.ndarray
678-
) -> list[cp.Constraint]:
690+
def define_terminal_state_constraints(self) -> list[cp.Constraint]:
679691
"""
680692
Define the terminal state constraints for the Data-Driven MPC
681693
formulation.
@@ -703,8 +715,8 @@ def define_terminal_state_constraints(
703715

704716
# Replicate steady-state vectors to match minimum realization
705717
# dimensions for constraint comparison
706-
u_sn = np.tile(u_s, (self.n, 1))
707-
y_sn = np.tile(y_s, (self.n, 1))
718+
u_sn = cp.vstack([self.u_s_param] * self.n)
719+
y_sn = cp.vstack([self.y_s_param] * self.n)
708720

709721
# Define terminal state constraints for Nominal and Robust MPC
710722
# based on Equations (3d) and (6c) of [1], respectively.
@@ -816,8 +828,10 @@ def define_cost_function(self) -> None:
816828

817829
# Define control-related cost
818830
control_cost = cp.quad_form(
819-
ubar_pred - np.tile(self.u_s, (self.L, 1)), self.R
820-
) + cp.quad_form(ybar_pred - np.tile(self.y_s, (self.L, 1)), self.Q)
831+
ubar_pred - cp.vstack([self.u_s_param] * self.L), self.R
832+
) + cp.quad_form(
833+
ybar_pred - cp.vstack([self.y_s_param] * self.L), self.Q
834+
)
821835

822836
# Define noise-related cost if controller type is Robust
823837
if self.controller_type == LTIDataDrivenMPCType.ROBUST:
@@ -1072,10 +1086,6 @@ def set_input_output_setpoints(
10721086
"""
10731087
Set the control and system setpoints of the Data-Driven MPC controller.
10741088
1075-
This method updates the control and system setpoints, `u_s` and `y_s`
1076-
to their provided values. Then, it reinitializes the controller to
1077-
redefine the Data-Driven MPC formulation.
1078-
10791089
Args:
10801090
u_s (np.ndarray): The setpoint for control inputs.
10811091
y_s (np.ndarray): The setpoint for system outputs.
@@ -1086,7 +1096,8 @@ def set_input_output_setpoints(
10861096
10871097
Note:
10881098
This method sets the values of the `u_s` and `y_s` attributes with
1089-
the provided new setpoints.
1099+
the provided new setpoints and updates the values of `u_s_param`
1100+
and `y_r_param` to update the data-driven MPC controller setpoint.
10901101
"""
10911102
# Validate input types and dimensions
10921103
if u_s.shape != self.u_s.shape:
@@ -1100,9 +1111,8 @@ def set_input_output_setpoints(
11001111
f"{self.y_s.shape}, but got {y_s.shape} instead."
11011112
)
11021113

1103-
# Update Input-Output setpoint pairs
1114+
# Update input-output setpoints and their parameter values
11041115
self.u_s = u_s
11051116
self.y_s = y_s
1106-
1107-
# Reinitialize Data-Driven MPC controller
1108-
self.initialize_data_driven_mpc()
1117+
self.u_s_param.value = u_s
1118+
self.y_s_param.value = y_s

direct_data_driven_mpc/nonlinear_data_driven_mpc_controller.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -709,16 +709,26 @@ def define_optimization_parameters(self) -> None:
709709
710710
This method initializes the following MPC parameters:
711711
712+
- Output setpoint: `y_r_param`.
712713
- Hankel matrices: `HLn1_u_param` and `HLn1_y_param`.
713714
- Past inputs and outputs: `u_past_param` and `y_past_param`.
714715
- Past input increments: `du_past_param` (if applicable).
715716
- Computed value of `alpha_Lin^sr(D_t)`: `alpha_sr_Lin_D_param` (if
716717
`alpha` is not regularized with respect to zero).
717718
718-
These parameters are updated at each MPC iteration. Using CVXPY
719-
`Parameter` objects allows efficient updates without the need of
720-
reformulating the MPC problem at every step.
719+
The value of `y_r_param` is initialized to `y_r`.
720+
721+
These parameters are updated at each MPC iteration, except for
722+
`y_r_param`, which must be manually updated when setting a new
723+
controller setpoint.
724+
725+
Using CVXPY `Parameter` objects allows efficient updates without the
726+
need of reformulating the MPC problem at every step.
721727
"""
728+
# Define the parameter for y_r and initialize its value
729+
self.y_r_param = cp.Parameter(self.y_r.shape, name="y_r")
730+
self.y_r_param.value = self.y_r
731+
722732
# H_{L+n+1}(u) - H_{L+n+1}(du)
723733
self.HLn1_u_param = cp.Parameter(self.HLn1_u.shape, name="HLn1_u")
724734

@@ -1014,7 +1024,7 @@ def define_cost_function(self) -> None:
10141024

10151025
# Define control-related cost
10161026
control_cost = self.tracking_cost + cp.quad_form(
1017-
self.y_s[: self.p] - self.y_r, self.S
1027+
self.y_s[: self.p] - self.y_r_param, self.S
10181028
)
10191029

10201030
# Define alpha-related cost
@@ -1360,10 +1370,6 @@ def set_output_setpoint(self, y_r: np.ndarray) -> None:
13601370
"""
13611371
Set the system output setpoint of the Data-Driven MPC controller.
13621372
1363-
This method updates the system setpoint `y_r` to the provided values.
1364-
Then, it reinitializes the controller to redefine the Data-Driven MPC
1365-
formulation.
1366-
13671373
Args:
13681374
y_r (np.ndarray): The setpoint for system outputs.
13691375
@@ -1372,7 +1378,8 @@ def set_output_setpoint(self, y_r: np.ndarray) -> None:
13721378
13731379
Note:
13741380
This method sets the values of the `y_r` attribute with the
1375-
provided new setpoint.
1381+
provided new setpoint and updates the value of `y_r_param`
1382+
to update the data-driven MPC controller setpoint.
13761383
"""
13771384
# Validate input types and dimensions
13781385
if y_r.shape != self.y_r.shape:
@@ -1381,11 +1388,9 @@ def set_output_setpoint(self, y_r: np.ndarray) -> None:
13811388
f"{self.y_s.shape}, but got {y_r.shape} instead."
13821389
)
13831390

1384-
# Update Output setpoint
1391+
# Update output setpoint and its parameter value
13851392
self.y_r = y_r
1386-
1387-
# Reinitialize Data-Driven MPC controller
1388-
self.initialize_data_driven_mpc()
1393+
self.y_r_param.value = y_r
13891394

13901395
def define_alpha_sr_Lin_Dt_prob(self) -> None:
13911396
"""
@@ -1442,7 +1447,7 @@ def define_alpha_sr_Lin_Dt_prob(self) -> None:
14421447

14431448
# Define objective
14441449
objective = cp.Minimize(
1445-
cp.quad_form(self.y_s[: self.p] - self.y_r, self.S)
1450+
cp.quad_form(self.y_s[: self.p] - self.y_r_param, self.S)
14461451
+ self.lamb_alpha_s * cp.norm(self.alpha_s, 2) ** 2
14471452
+ self.lamb_sigma_s * cp.norm(self.sigma_s, 2) ** 2
14481453
)

requirements-dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
numpy
22
matplotlib>=3.9.0
33
clarabel==0.10.0
4-
cvxpy
4+
cvxpy==1.6.5
55
tqdm
66
PyYAML
77
PyQt6

tests/config/controllers/test_nonlinear_dd_mpc_params.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ test_nonlinear_dd_mpc_params:
33
n: 4
44
N: 50
55
L: 10
6-
Q_weights: 1
6+
Q_weights: 10
77
R_weights: 1
8-
S_weights: 10
8+
S_weights: 100
99
lambda_alpha: 10.0
1010
lambda_sigma: 1.0e7
1111
U:
@@ -15,7 +15,7 @@ test_nonlinear_dd_mpc_params:
1515
u_range:
1616
- [-1.0, 1.0]
1717
alpha_reg_type: 0
18-
lambda_alpha_s: 1e-3
18+
lambda_alpha_s: 1e-5
1919
lambda_sigma_s: 1.0e7
2020
y_r: [3.0]
2121
ext_out_incr_in: true

tests/test_controllers/conftest.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717

1818
@pytest.fixture
19-
@patch.object(LTIDataDrivenMPCController, "initialize_data_driven_mpc")
19+
@patch.object(LTIDataDrivenMPCController, "get_optimal_control_input")
20+
@patch.object(LTIDataDrivenMPCController, "solve_mpc_problem")
2021
def dummy_lti_controller(
21-
_: Mock,
22+
mock_solve_mpc_problem: Mock,
23+
mock_get_optimal_control_input: Mock,
2224
dummy_lti_controller_data: tuple[
2325
LTIDataDrivenMPCParams, np.ndarray, np.ndarray
2426
],
@@ -27,6 +29,9 @@ def dummy_lti_controller(
2729
m = u_d.shape[1]
2830
p = y_d.shape[1]
2931

32+
# Patch controller methods to bypass solver status checks
33+
mock_get_optimal_control_input.return_value = np.ones((1,))
34+
3035
dummy_lti_controller = LTIDataDrivenMPCController(
3136
n=controller_params["n"],
3237
m=m,
@@ -55,9 +60,13 @@ def dummy_lti_controller(
5560

5661

5762
@pytest.fixture
58-
@patch.object(LTIDataDrivenMPCController, "initialize_data_driven_mpc")
63+
@patch.object(NonlinearDataDrivenMPCController, "get_optimal_control_input")
64+
@patch.object(NonlinearDataDrivenMPCController, "solve_mpc_problem")
65+
@patch.object(NonlinearDataDrivenMPCController, "solve_alpha_sr_Lin_Dt")
5966
def dummy_nonlinear_controller(
60-
_: Mock,
67+
mock_solve_alpha_problem: Mock,
68+
mock_solve_mpc_problem: Mock,
69+
mock_get_optimal_control_input: Mock,
6170
dummy_nonlinear_controller_data: tuple[
6271
NonlinearDataDrivenMPCParams, np.ndarray, np.ndarray
6372
],
@@ -66,6 +75,13 @@ def dummy_nonlinear_controller(
6675
m = u.shape[1]
6776
p = y.shape[1]
6877

78+
# Patch controller methods to bypass solver status checks
79+
N = controller_params["N"]
80+
L = controller_params["L"]
81+
n = controller_params["n"]
82+
mock_solve_alpha_problem.return_value = np.zeros((N - L - n, 1))
83+
mock_get_optimal_control_input.return_value = np.ones((1,))
84+
6985
dummy_nonlinear_controller = NonlinearDataDrivenMPCController(
7086
n=controller_params["n"],
7187
m=m,

tests/test_controllers/test_lti_dd_mpc_controller_unit.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,10 @@ def test_lti_dd_mpc_controller_invalid_params(
187187

188188

189189
@pytest.mark.parametrize("valid_dimensions", [True, False])
190-
@patch.object(LTIDataDrivenMPCController, "get_optimal_control_input")
191190
def test_lti_store_input_output_measurement(
192-
mock_controller_get_optimal_input: Mock,
193191
valid_dimensions: bool,
194192
dummy_lti_controller: LTIDataDrivenMPCController,
195193
) -> None:
196-
# Patch optimal control input retrieval to bypass solver status checks
197-
mock_controller_get_optimal_input.return_value = np.ones((1,))
198-
199194
# Get dummy LTI data-driven MPC controller
200195
controller = dummy_lti_controller
201196

@@ -217,15 +212,10 @@ def test_lti_store_input_output_measurement(
217212

218213

219214
@pytest.mark.parametrize("valid_dimensions", [True, False])
220-
@patch.object(LTIDataDrivenMPCController, "get_optimal_control_input")
221215
def test_lti_set_past_input_output_data(
222-
mock_controller_get_optimal_input: Mock,
223216
valid_dimensions: bool,
224217
dummy_lti_controller: LTIDataDrivenMPCController,
225218
) -> None:
226-
# Patch optimal control input retrieval to bypass solver status checks
227-
mock_controller_get_optimal_input.return_value = np.ones((1,))
228-
229219
# Get dummy LTI data-driven MPC controller
230220
controller = dummy_lti_controller
231221

@@ -247,9 +237,7 @@ def test_lti_set_past_input_output_data(
247237

248238

249239
@pytest.mark.parametrize("valid_dimensions", [True, False])
250-
@patch.object(LTIDataDrivenMPCController, "initialize_data_driven_mpc")
251240
def test_lti_set_input_output_setpoints(
252-
mock_controller_init: Mock,
253241
valid_dimensions: bool,
254242
dummy_lti_controller: LTIDataDrivenMPCController,
255243
) -> None:
@@ -266,7 +254,6 @@ def test_lti_set_input_output_setpoints(
266254
assert np.allclose(controller.u_s, u_s)
267255
assert np.allclose(controller.y_s, y_s)
268256

269-
mock_controller_init.assert_called()
270257
else:
271258
u_s = np.ones((controller.u_s.shape[0] + 1, 1))
272259
y_s = np.ones((controller.y_s.shape[0] + 1, 1))

tests/test_controllers/test_nonlinear_dd_mpc_controller_unit.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,10 @@ def test_nonlinear_dd_mpc_controller_invalid_params(
149149

150150

151151
@pytest.mark.parametrize("valid_dimensions", [True, False])
152-
@patch.object(NonlinearDataDrivenMPCController, "get_optimal_control_input")
153152
def test_nonlinear_store_input_output_measurement(
154-
mock_controller_get_optimal_input: Mock,
155153
valid_dimensions: bool,
156154
dummy_nonlinear_controller: NonlinearDataDrivenMPCController,
157155
) -> None:
158-
# Patch optimal control input retrieval to bypass solver status checks
159-
mock_controller_get_optimal_input.return_value = np.ones((1,))
160-
161156
# Get dummy nonlinear data-driven MPC controller
162157
controller = dummy_nonlinear_controller
163158

@@ -179,15 +174,10 @@ def test_nonlinear_store_input_output_measurement(
179174

180175

181176
@pytest.mark.parametrize("valid_dimensions", [True, False])
182-
@patch.object(NonlinearDataDrivenMPCController, "get_optimal_control_input")
183177
def test_nonlinear_set_input_output_data(
184-
mock_controller_get_optimal_input: Mock,
185178
valid_dimensions: bool,
186179
dummy_nonlinear_controller: NonlinearDataDrivenMPCController,
187180
) -> None:
188-
# Patch optimal control input retrieval to bypass solver status checks
189-
mock_controller_get_optimal_input.return_value = np.ones((1,))
190-
191181
# Get dummy nonlinear data-driven MPC controller
192182
controller = dummy_nonlinear_controller
193183

@@ -209,9 +199,7 @@ def test_nonlinear_set_input_output_data(
209199

210200

211201
@pytest.mark.parametrize("valid_dimensions", [True, False])
212-
@patch.object(NonlinearDataDrivenMPCController, "initialize_data_driven_mpc")
213202
def test_nonlinear_set_output_setpoint(
214-
mock_controller_init: Mock,
215203
valid_dimensions: bool,
216204
dummy_nonlinear_controller: NonlinearDataDrivenMPCController,
217205
) -> None:
@@ -226,7 +214,6 @@ def test_nonlinear_set_output_setpoint(
226214

227215
assert np.allclose(controller.y_r, y_r)
228216

229-
mock_controller_init.assert_called()
230217
else:
231218
y_r = np.ones((controller.y_r.shape[0] + 1, 1))
232219

0 commit comments

Comments
 (0)