mpc-control 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ from .plant import Plant, LoggedPlant
2
+ from .kalman import Ekf, Ukf
3
+ from .rls import Rls
4
+ from .discrete import LtiSystem, AtiSystem, HomogeneousSystem, NonlinearSystem
5
+ from .mpc import Mpc
6
+
7
+
8
+ def _get_version() -> str:
9
+ """Try to get the installed package version.
10
+
11
+ If the package is not installed (e.g., running from source in
12
+ development mode), fall back to "dev".
13
+ """
14
+ from importlib.metadata import version, PackageNotFoundError
15
+ try:
16
+ return version("mpc-control")
17
+ except PackageNotFoundError:
18
+ return "dev"
19
+
20
+
21
+ __version__ = _get_version()
@@ -0,0 +1,632 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Discrete-time system models for Model Predictive Control.
4
+
5
+ This module provides abstract base classes and concrete
6
+ implementations for discrete-time systems. These systems define the
7
+ state transition, control input, and output relationships used by the
8
+ MPC controller.
9
+
10
+ Classes:
11
+ DiscreteSystem: abstract base class for discrete-time systems.
12
+ LtiSystem: discrete linear time-invariant system.
13
+ NonlinearSystem: generalized discrete non-linear system.
14
+ HomogeneousSystem: discrete homogeneous non-linear system.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import abc
20
+ from typing import Optional, Callable, override, final
21
+ import numpy as np
22
+
23
+
24
+ __all__ = ('LtiSystem', 'AtiSystem',
25
+ 'HomogeneousSystem', 'NonlinearSystem')
26
+
27
+
28
+ class Discrete(abc.ABC):
29
+ """Abstract base class for discrete-time systems.
30
+
31
+ The system can be written as:
32
+ x[n+1] = f(x[n], u[n])
33
+ y[n] = g(x[n])
34
+ """
35
+
36
+ @abc.abstractproperty
37
+ def n_state(self) -> int:
38
+ """Dimension of state vector."""
39
+ ...
40
+
41
+ @abc.abstractproperty
42
+ def n_control(self) -> int:
43
+ """Dimension of control vector."""
44
+ ...
45
+
46
+ @abc.abstractproperty
47
+ def n_output(self) -> int:
48
+ """Dimension of output vector."""
49
+ ...
50
+
51
+ @abc.abstractmethod
52
+ def _linearize_transition(self,
53
+ state: Optional[np.ndarray] = None,
54
+ control: Optional[np.ndarray] = None
55
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
56
+ """Linearize the state transition function.
57
+
58
+ Args:
59
+ state: current state vector of shape (n_state, ).
60
+ control: current control input of shape (n_control, ).
61
+
62
+ Returns:
63
+ tuple[np.ndarray, np.ndarray, np.ndarray]: the transition
64
+ matrix A, control matrix B, and disturbance w.
65
+
66
+ """
67
+ ...
68
+
69
+ @abc.abstractmethod
70
+ def _linearize_output(self,
71
+ state: Optional[np.ndarray] = None,
72
+ ) -> tuple[np.ndarray, np.ndarray]:
73
+ """Linearize the output function.
74
+
75
+ Args:
76
+ state: current state vector of shape (n_state, ).
77
+
78
+ Returns:
79
+ tuple[np.ndarray, np.ndarray]: the output matrix C and the
80
+ output disturbance vector v.
81
+ """
82
+ ...
83
+
84
+ def linearize(self,
85
+ state: Optional[np.ndarray] = None,
86
+ control: Optional[np.ndarray] = None
87
+ ) -> AffineTimeInvariant:
88
+ """Return the lti system based on given states.
89
+
90
+ Args:
91
+ state: current state vector of shape (n_state, ).
92
+ control: current control input of shape (n_control, ).
93
+
94
+ Returns:
95
+ LtiSystem: the linearized system.
96
+ """
97
+ transition_matrix, control_matrix, state_disturbance_vector = \
98
+ self._linearize_transition(state, control)
99
+ output_matrix, output_disturbance_vector = \
100
+ self._linearize_output(state)
101
+ return AtiSystem(transition_matrix,
102
+ control_matrix,
103
+ state_disturbance_vector,
104
+ output_matrix,
105
+ output_disturbance_vector)
106
+
107
+ def _get_state_one_step(self,
108
+ state: np.ndarray,
109
+ control: np.ndarray) -> np.ndarray:
110
+ """Evaluate the next state vector.
111
+
112
+ Args:
113
+ state: current state vector of shape (n_state, ).
114
+ control: current control input of shape (n_control, ).
115
+
116
+ Returns:
117
+ np.ndarray: the state sequence of shape (n_state,).
118
+ """
119
+ a, b, w = self._linearize_transition(state, control)
120
+ return a @ state + b @ control + w
121
+
122
+ def get_state(self,
123
+ initial_state: np.ndarray,
124
+ controls: np.ndarray
125
+ ) -> np.ndarray:
126
+ """Evaluate the states based on given input.
127
+
128
+ Args:
129
+ initial_state: the initial state of shape (n_state, ).
130
+ controls: the control input of shape (n_steps, n_control).
131
+
132
+ Returns:
133
+ np.ndarray: the state sequence of shape (n_steps, n_state).
134
+ """
135
+ n = controls.shape[0]
136
+ xs = np.zeros([n, self.n_state])
137
+ state = initial_state
138
+ for (i, control) in enumerate(controls):
139
+ next_state = self._get_state_one_step(state, control)
140
+ xs[i] = next_state
141
+ state = next_state
142
+ return xs
143
+
144
+ def _get_output_one_step(self,
145
+ state: np.ndarray
146
+ ) -> np.ndarray:
147
+ """Evaluate the output vector for a single step.
148
+
149
+ Args:
150
+ state: current state vector of shape (n_state, ).
151
+
152
+ Returns:
153
+ np.ndarray: the output vector of shape (n_output, ).
154
+ """
155
+ c, v = self._linearize_output(state)
156
+ return c @ state + v
157
+
158
+ def get_output(self,
159
+ states: np.ndarray
160
+ ) -> np.ndarray:
161
+ """Evaluate the outputs based on given states.
162
+
163
+ Args:
164
+ states: the state sequence of shape (n_steps, n_state),
165
+ typically obtained from get_state().
166
+
167
+ Returns:
168
+ np.ndarray: the output sequence of shape (n_steps, n_output).
169
+ """
170
+ n = states.shape[0]
171
+ ys = np.zeros([n, self.n_output])
172
+ for (i, state) in enumerate(states):
173
+ ys[i] = self._get_output_one_step(state)
174
+ return ys
175
+
176
+
177
+ class AffineTimeInvariant(Discrete):
178
+ """Base class (trait) for affine time invariant (ATI) systems.
179
+
180
+ x[n+1] = A @ x[n] + B @ u[n] + w
181
+ y[n] = C @ x[n]
182
+ """
183
+
184
+ @abc.abstractproperty
185
+ def transition_matrix(self) -> np.ndarray:
186
+ """Return the state transition matrix A."""
187
+ ...
188
+
189
+ @abc.abstractproperty
190
+ def control_matrix(self) -> np.ndarray:
191
+ """Return the control matrix B."""
192
+ ...
193
+
194
+ @abc.abstractproperty
195
+ def state_disturbance_vector(self) -> np.ndarray:
196
+ """Return the state disturbance vector w."""
197
+ ...
198
+
199
+ @abc.abstractproperty
200
+ def output_disturbance_vector(self) -> np.ndarray:
201
+ """Return the output disturbance vector v."""
202
+ ...
203
+
204
+ @abc.abstractproperty
205
+ def output_matrix(self) -> np.ndarray:
206
+ """Return the output matrix C."""
207
+ ...
208
+
209
+ @override
210
+ def _linearize_transition(self,
211
+ state: Optional[np.ndarray] = None,
212
+ control: Optional[np.ndarray] = None
213
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
214
+ """Linearize the state transition function.
215
+
216
+ Args:
217
+ state: current state vector of shape (n_state, ).
218
+ control: current control input of shape (n_control, ).
219
+
220
+ Returns:
221
+ tuple[np.ndarray, np.ndarray, np.ndarray]: the transition
222
+ matrix A, control matrix B, and disturbance w.
223
+
224
+ """
225
+ return (self.transition_matrix,
226
+ self.control_matrix,
227
+ self.state_disturbance_vector)
228
+
229
+ @override
230
+ def _linearize_output(self,
231
+ state: Optional[np.ndarray] = None,
232
+ ) -> tuple[np.ndarray, np.ndarray]:
233
+ """Linearize the output function.
234
+
235
+ Args:
236
+ state: current state vector of shape (n_state, ).
237
+
238
+ Returns:
239
+ tuple[np.ndarray, np.ndarray]: the output matrix C and the
240
+ output disturbance vector v.
241
+ """
242
+ return (self.output_matrix, self.output_disturbance_vector)
243
+
244
+
245
+ @final
246
+ class AtiSystem(AffineTimeInvariant):
247
+ """Affine time invariant system.
248
+
249
+ x[n+1] = A @ x[n] + B @ u[n] + w
250
+ y[n] = C @ x[n] + v
251
+ """
252
+
253
+ def __init__(self,
254
+ transition_matrix: np.ndarray,
255
+ control_matrix: np.ndarray,
256
+ state_disturbance_vector: np.ndarray,
257
+ output_matrix: np.ndarray,
258
+ output_disturbance_vector: np.ndarray
259
+ ):
260
+ """Initialize the ATI system.
261
+
262
+ Args:
263
+ transition_matrix: the state transition matrix A of shape
264
+ (n_state, n_state).
265
+ control_matrix: the control matrix B of shape
266
+ (n_state, n_control).
267
+ state_disturbance_vector: the state disturbance vector w of shape
268
+ (n_state,).
269
+ output_matrix: the output matrix C of shape
270
+ (n_output, n_state).
271
+ output_disturbance_vector: the output disturbance vector v of
272
+ shape (n_output,).
273
+ """
274
+ self._a = np.asarray(transition_matrix)
275
+ self._b = np.asarray(control_matrix)
276
+ self._w = np.asarray(state_disturbance_vector)
277
+ self._c = np.asarray(output_matrix)
278
+ self._v = np.asarray(output_disturbance_vector)
279
+ self._n_state = self._a.shape[0]
280
+ self._n_control = self._b.shape[1]
281
+ self._n_output = self._c.shape[0]
282
+
283
+ @override
284
+ @property
285
+ def n_state(self) -> int:
286
+ """Dimension of state vector."""
287
+ return self._n_state
288
+
289
+ @override
290
+ @property
291
+ def n_control(self) -> int:
292
+ """Dimension of control vector."""
293
+ return self._n_control
294
+
295
+ @override
296
+ @property
297
+ def n_output(self) -> int:
298
+ """Dimension of output vector."""
299
+ return self._n_output
300
+
301
+ @override
302
+ @property
303
+ def transition_matrix(self) -> np.ndarray:
304
+ """Return the state transition matrix A."""
305
+ return self._a
306
+
307
+ @override
308
+ @property
309
+ def control_matrix(self) -> np.ndarray:
310
+ """Return the control matrix B."""
311
+ return self._b
312
+
313
+ @override
314
+ @property
315
+ def state_disturbance_vector(self) -> np.ndarray:
316
+ """Return the state disturbance vector w."""
317
+ return self._w
318
+
319
+ @override
320
+ @property
321
+ def output_disturbance_vector(self) -> np.ndarray:
322
+ """Return the output disturbance vector v."""
323
+ return self._v
324
+
325
+ @override
326
+ @property
327
+ def output_matrix(self) -> np.ndarray:
328
+ """Return the output matrix C."""
329
+ return self._c
330
+
331
+
332
+ @final
333
+ class LtiSystem(AffineTimeInvariant):
334
+ """
335
+ Discrete Linear Time-Invariant System.
336
+
337
+ Equation:
338
+ x[n+1] = A @ x[n] + B @ u[n]
339
+ y[n] = C @ x[n]
340
+ """
341
+
342
+ def __init__(self,
343
+ transition_matrix: np.ndarray,
344
+ control_matrix: np.ndarray,
345
+ output_matrix: np.ndarray
346
+ ):
347
+ """Initialize the LTI system.
348
+
349
+ Args:
350
+ transition_matrix: the state transition matrix A of shape
351
+ (n_state, n_state).
352
+ control_matrix: the control matrix B of shape
353
+ (n_state, n_control).
354
+ output_matrix: the output matrix C of shape
355
+ (n_output, n_state).
356
+ """
357
+ self._a = np.asarray(transition_matrix)
358
+ self._b = np.asarray(control_matrix)
359
+ self._c = np.asarray(output_matrix)
360
+ self._n_state = self._a.shape[0]
361
+ self._n_control = self._b.shape[1]
362
+ self._n_output = self._c.shape[0]
363
+
364
+ @override
365
+ @property
366
+ def n_state(self) -> int:
367
+ """Dimension of state vector."""
368
+ return self._n_state
369
+
370
+ @override
371
+ @property
372
+ def n_control(self) -> int:
373
+ """Dimension of control vector."""
374
+ return self._n_control
375
+
376
+ @override
377
+ @property
378
+ def n_output(self) -> int:
379
+ """Dimension of output vector."""
380
+ return self._n_output
381
+
382
+ @override
383
+ @property
384
+ def transition_matrix(self) -> np.ndarray:
385
+ """Return the state transition matrix A."""
386
+ return self._a
387
+
388
+ @override
389
+ @property
390
+ def control_matrix(self) -> np.ndarray:
391
+ """Return the control matrix B."""
392
+ return self._b
393
+
394
+ @override
395
+ @property
396
+ def state_disturbance_vector(self) -> np.ndarray:
397
+ """Return the state disturbance vector w."""
398
+ return np.zeros([self.n_state])
399
+
400
+ @override
401
+ @property
402
+ def output_disturbance_vector(self) -> np.ndarray:
403
+ """Return the output disturbance vector v."""
404
+ return np.zeros([self.n_output])
405
+
406
+ @override
407
+ @property
408
+ def output_matrix(self) -> np.ndarray:
409
+ """Return the output matrix C."""
410
+ return self._c
411
+
412
+
413
+ @final
414
+ class HomogeneousSystem(Discrete):
415
+ """
416
+ Discrete non-linear homogeneous system.
417
+
418
+ Equation:
419
+ x[n+1] = A(x[n], u[n]) @ x[n] + B(x[n], u[n]) @ u[n]
420
+ y[n] = C(x[n]) @ x[n]
421
+ """
422
+
423
+ def __init__(
424
+ self,
425
+ n_state: int,
426
+ n_control: int,
427
+ n_output: int,
428
+ transition_matrix: Callable[[np.ndarray, np.ndarray],
429
+ np.ndarray],
430
+ control_matrix: Callable[[np.ndarray, np.ndarray],
431
+ np.ndarray],
432
+ output_matrix: Callable[[np.ndarray],
433
+ np.ndarray]):
434
+ """Initialize the discrete non-linear system with disturbance.
435
+
436
+ Args:
437
+ n_state: dimension of the state vector.
438
+ n_control: dimension of the control vector.
439
+ n_output: dimension of the output vector.
440
+ transition_matrix: callable that returns the state transition
441
+ matrix A. Signature: A(state, control) -> np.ndarray of
442
+ shape (n_state, n_state).
443
+ control_matrix: callable that returns the control matrix B.
444
+ Signature: B(state, control) -> np.ndarray of shape
445
+ (n_state, n_control).
446
+ output_matrix: callable that returns the output matrix C.
447
+ Signature: C(state) -> np.ndarray of shape
448
+ (n_output, n_state).
449
+ """
450
+ self._n_state = n_state
451
+ self._n_control = n_control
452
+ self._n_output = n_output
453
+ self._a = transition_matrix
454
+ self._b = control_matrix
455
+ self._c = output_matrix
456
+
457
+ @override
458
+ @property
459
+ def n_state(self) -> int:
460
+ """Dimension of state vector."""
461
+ return self._n_state
462
+
463
+ @override
464
+ @property
465
+ def n_control(self) -> int:
466
+ """Dimension of control vector."""
467
+ return self._n_control
468
+
469
+ @override
470
+ @property
471
+ def n_output(self) -> int:
472
+ """Dimension of output vector."""
473
+ return self._n_output
474
+
475
+ @override
476
+ def _linearize_transition(self,
477
+ state: Optional[np.ndarray] = None,
478
+ control: Optional[np.ndarray] = None
479
+ ) -> tuple[np.ndarray,
480
+ np.ndarray,
481
+ np.ndarray]:
482
+ """Linearize the state transition function.
483
+
484
+ Args:
485
+ state: current state vector of shape (n_state, ).
486
+ control: current control input of shape (n_control, ).
487
+
488
+ Returns:
489
+ tuple[np.ndarray, np.ndarray, np.ndarray]: the transition
490
+ matrix A, control matrix B, and disturbance w.
491
+ """
492
+ if state is None:
493
+ raise ValueError('state can not be None')
494
+ if control is None:
495
+ raise ValueError('control can not be None')
496
+ return (self._a(state, control),
497
+ self._b(state, control),
498
+ np.zeros([self.n_state]))
499
+
500
+ @override
501
+ def _linearize_output(self,
502
+ state: Optional[np.ndarray] = None,
503
+ ) -> tuple[np.ndarray, np.ndarray]:
504
+ """Linearize the output function.
505
+
506
+ Args:
507
+ state: current state vector of shape (n_state, ).
508
+
509
+ Returns:
510
+ tuple[np.ndarray, np.ndarray]: the output matrix C and the
511
+ output disturbance vector v.
512
+ """
513
+ if state is None:
514
+ raise ValueError('state can not be None')
515
+ return (self._c(state), np.zeros(self.n_output))
516
+
517
+
518
+ @final
519
+ class NonlinearSystem(Discrete):
520
+ """
521
+ Discrete non-linear System.
522
+
523
+ Equation:
524
+ x[n+1] = A(x[n], u[n]) @ x[n] + B(x[n], u[n]) @ u[n] + w(x[n], u[n])
525
+ y[n] = C(x[n]) @ x[n] + v(x[n])
526
+ """
527
+
528
+ def __init__(
529
+ self,
530
+ n_state: int,
531
+ n_control: int,
532
+ n_output: int,
533
+ transition_matrix: Callable[[np.ndarray, np.ndarray],
534
+ np.ndarray],
535
+ control_matrix: Callable[[np.ndarray, np.ndarray],
536
+ np.ndarray],
537
+ state_disturbance_vector: Callable[[np.ndarray, np.ndarray],
538
+ np.ndarray],
539
+ output_matrix: Callable[[np.ndarray],
540
+ np.ndarray],
541
+ output_disturbance_vector: Callable[[np.ndarray],
542
+ np.ndarray]):
543
+ """Initialize the discrete non-linear system with disturbance.
544
+
545
+ Args:
546
+ n_state: dimension of the state vector.
547
+ n_control: dimension of the control vector.
548
+ n_output: dimension of the output vector.
549
+ transition_matrix: callable that returns the state transition
550
+ matrix A. Signature: A(state, control) -> np.ndarray of
551
+ shape (n_state, n_state).
552
+ control_matrix: callable that returns the control matrix B.
553
+ Signature: B(state, control) -> np.ndarray of shape
554
+ (n_state, n_control).
555
+ state_disturbance_vector: callable that returns the state
556
+ disturbance vector w. Signature: w(state, control) ->
557
+ np.ndarray of shape (n_state,).
558
+ output_matrix: callable that returns the output matrix C.
559
+ Signature: C(state) -> np.ndarray of shape
560
+ (n_output, n_state).
561
+ output_disturbance_vector: callable that returns the output
562
+ disturbance vector v. Signature: v(state) -> np.ndarray
563
+ of shape (n_output,).
564
+ """
565
+ self._n_state = n_state
566
+ self._n_control = n_control
567
+ self._n_output = n_output
568
+ self._a = transition_matrix
569
+ self._b = control_matrix
570
+ self._w = state_disturbance_vector
571
+ self._c = output_matrix
572
+ self._v = output_disturbance_vector
573
+
574
+ @override
575
+ @property
576
+ def n_state(self) -> int:
577
+ """Dimension of state vector."""
578
+ return self._n_state
579
+
580
+ @override
581
+ @property
582
+ def n_control(self) -> int:
583
+ """Dimension of control vector."""
584
+ return self._n_control
585
+
586
+ @override
587
+ @property
588
+ def n_output(self) -> int:
589
+ """Dimension of output vector."""
590
+ return self._n_output
591
+
592
+ @override
593
+ def _linearize_transition(self,
594
+ state: Optional[np.ndarray] = None,
595
+ control: Optional[np.ndarray] = None
596
+ ) -> tuple[np.ndarray,
597
+ np.ndarray,
598
+ np.ndarray]:
599
+ """Linearize the state transition function.
600
+
601
+ Args:
602
+ state: current state vector of shape (n_state, ).
603
+ control: current control input of shape (n_control, ).
604
+
605
+ Returns:
606
+ tuple[np.ndarray, np.ndarray, np.ndarray]: the transition
607
+ matrix A, control matrix B, and disturbance w.
608
+ """
609
+ if state is None:
610
+ raise ValueError('state can not be None')
611
+ if control is None:
612
+ raise ValueError('control can not be None')
613
+ return (self._a(state, control),
614
+ self._b(state, control),
615
+ self._w(state, control))
616
+
617
+ @override
618
+ def _linearize_output(self,
619
+ state: Optional[np.ndarray] = None,
620
+ ) -> tuple[np.ndarray, np.ndarray]:
621
+ """Linearize the output function.
622
+
623
+ Args:
624
+ state: current state vector of shape (n_state, ).
625
+
626
+ Returns:
627
+ tuple[np.ndarray, np.ndarray]: the output matrix C and the
628
+ output disturbance vector v.
629
+ """
630
+ if state is None:
631
+ raise ValueError('state can not be None')
632
+ return (self._c(state), self._v(state))