mpc-control 0.2.0__tar.gz → 0.2.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mpc-control
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A Python library for Model Predictive Control (MPC)
5
5
  Author-email: Zhen Ni <z.ni@hotmail.com>
6
6
  License-Expression: MIT
@@ -9,7 +9,7 @@ Project-URL: Issues, https://github.com/Zhen-Ni/mpc-control/issues
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Topic :: Scientific/Engineering
12
- Requires-Python: >=3.9
12
+ Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: numpy>=2.0
@@ -42,7 +42,7 @@ pip install mpc-control
42
42
  ```
43
43
 
44
44
  **Requirements:**
45
- - Python 3.9+ (Tested with Python 3.12)
45
+ - Python 3.12+
46
46
  - NumPy, SciPy, OSQP
47
47
 
48
48
  ## 🚀 Quick Start
@@ -51,7 +51,7 @@ Here is a basic example of how to define a system and solve an MPC problem:
51
51
 
52
52
  ```python
53
53
  import numpy as np
54
- import mpc_control as mpc
54
+ import mpc
55
55
 
56
56
  # 1. Define a discrete LTI system
57
57
  # x[n+1] = A x[n] + B u[n]
@@ -23,7 +23,7 @@ pip install mpc-control
23
23
  ```
24
24
 
25
25
  **Requirements:**
26
- - Python 3.9+ (Tested with Python 3.12)
26
+ - Python 3.12+
27
27
  - NumPy, SciPy, OSQP
28
28
 
29
29
  ## 🚀 Quick Start
@@ -32,7 +32,7 @@ Here is a basic example of how to define a system and solve an MPC problem:
32
32
 
33
33
  ```python
34
34
  import numpy as np
35
- import mpc_control as mpc
35
+ import mpc
36
36
 
37
37
  # 1. Define a discrete LTI system
38
38
  # x[n+1] = A x[n] + B u[n]
@@ -134,14 +134,14 @@ class Ekf:
134
134
  """
135
135
  if self._linearized is None:
136
136
  raise RuntimeError('Should call `predict` before updating.')
137
- c = self._linearized.output_matrix
137
+ c, v = self._system._linearize_output(self._x)
138
138
  # Calculate innovation covariance `S`.
139
139
  S = c @ self._p @ c.T + r
140
140
  # Calculate Kalman gain `k`.
141
141
  # k = p @ C.T @ inv(S) is equivalent to k.T = solve(S, C @ p)
142
142
  k = np.linalg.solve(S, c @ self._p).T
143
143
  # Calculate the updated state vector `x`.
144
- x = self._x + k @ (np.asarray(z) - c @ self._x)
144
+ x = self._x + k @ (np.asarray(z) - c @ self._x - v)
145
145
  # Calculate the updated uncertainty `p` for the state vector.
146
146
  T = np.eye(k.shape[0]) - k @ c
147
147
  p = T @ self._p @ T.T + k @ r @ k.T
@@ -678,74 +678,54 @@ class Mpc:
678
678
 
679
679
  self._qp_internal.q = q
680
680
 
681
- def solve(self,
682
- target_output: np.ndarray,
683
- initial_state: np.ndarray,
684
- previous_control: Optional[np.ndarray] = None,
685
- state_ref: Optional[np.ndarray] = None,
686
- control_ref: Optional[np.ndarray] = None,
687
- max_iter: Optional[int] = None,
688
- eps_abs: Optional[float] = None,
689
- eps_rel: Optional[float] = None
690
- ) -> Optional[np.ndarray]:
691
- """Solve the mpc problem.
692
-
693
- Args:
694
- initial_state: Initial state (x_0).
695
- previous_control: The control input in the previous
696
- timestep (u_{-1}).
697
- target_output: Reference output sequence Y_ref. Note that
698
- this corresponds to the outputs from step 1 to step N
699
- (y_1 to y_N), as the output at the current step 0
700
- (y_0) cannot be influenced by future controls.
701
- state_ref: Reference state for system linearization. This
702
- corresponds to the states from step 0 to step N-1 (x_0
703
- to x_{N-1}).
704
- control_ref: Reference control for system
705
- linearization. This corresponds to the controls from
706
- step 0 to step N-1 (u_0 to u_{N-1}).
707
- max_iter: Maximum iterations for OSQP solver.
708
- eps_abs: Absolute convergence tolerance for OSQP solver.
709
- eps_rel: Relative convergence tolerance for OSQP solver.
710
-
711
- Returns:
712
- The optimal control sequence U with shape (horizon,
713
- n_control), corresponding to the controls from step 0 to
714
- step N-1 (u_0 to u_{N-1}), or None if the problem is not
715
- solved successfully.
716
-
717
- """
718
- use_cached_p = (
719
- self._qp_internal and
720
- isinstance(self._system, AffineTimeInvariant))
721
- use_cached_a = (
722
- (not self._qp_constraint.modified) and
723
- isinstance(self._system, AffineTimeInvariant))
724
-
725
- if previous_control is None:
726
- if (self._control_delta_weighting is None and
727
- self._qp_constraint.control_delta_bound is None):
728
- # Set to empty because it will not be used anyway.
729
- previous_control = np.empty(self._system.n_control)
730
- else:
731
- raise ValueError(
732
- 'previous_control must be provided if '
733
- 'control changing rate is involved.')
734
-
681
+ def _validate_previous_control(
682
+ self,
683
+ previous_control: Optional[np.ndarray]) -> np.ndarray:
684
+ if previous_control is not None:
685
+ return previous_control
686
+ # Use a placeholder for previous control, as it will not be
687
+ # used in this case.
688
+ if (self._control_delta_weighting is None and
689
+ self._qp_constraint.control_delta_bound is None):
690
+ return np.empty(self._system.n_control)
691
+ raise ValueError(
692
+ 'previous_control must be provided if '
693
+ 'control changing rate is involved.')
694
+
695
+ def _prepare_qp_matrices(
696
+ self,
697
+ target_output: np.ndarray,
698
+ initial_state: np.ndarray,
699
+ previous_control: np.ndarray,
700
+ state_ref: Optional[np.ndarray],
701
+ control_ref: Optional[np.ndarray],
702
+ use_cached_p: bool) -> None:
735
703
  if use_cached_p:
736
- self._update_qp(target_output,
737
- initial_state, previous_control)
704
+ self._update_qp(target_output, initial_state, previous_control)
738
705
  else:
739
- self._build_qp(target_output,
740
- initial_state, previous_control,
706
+ self._build_qp(target_output, initial_state, previous_control,
741
707
  state_ref, control_ref)
742
708
 
709
+ def _prepare_constraints(
710
+ self,
711
+ initial_state: np.ndarray,
712
+ previous_control: np.ndarray,
713
+ use_cached_a: bool) -> None:
743
714
  if use_cached_a:
744
715
  self._update_constraints(initial_state, previous_control)
745
716
  else:
746
717
  self._build_constraints(initial_state, previous_control)
747
718
 
719
+ def _setup_and_solve_osqp(
720
+ self,
721
+ max_iter: Optional[int],
722
+ eps_abs: Optional[float],
723
+ eps_rel: Optional[float],
724
+ control_ref: Optional[np.ndarray],
725
+ use_cached_p: bool,
726
+ use_cached_a: bool) -> Optional[np.ndarray]:
748
727
  assert self._qp_internal is not None # Make mypy happy
728
+
749
729
  p = self._qp_internal.csc_p
750
730
  q = self._qp_internal.q
751
731
  a = self._qp_constraint.a
@@ -773,9 +753,64 @@ class Mpc:
773
753
 
774
754
  if control_ref is not None:
775
755
  self._osqp.warm_start(x=np.asarray(control_ref).reshape(-1))
756
+
776
757
  res = self._osqp.solve(raise_error=False)
777
758
  self._result = res
778
759
  if res.info.status == 'solved':
779
- u = res.x.reshape(self._horizon, self._system.n_control)
780
- return u
760
+ return res.x.reshape(self._horizon, self._system.n_control)
781
761
  return None
762
+
763
+ def solve(self,
764
+ target_output: np.ndarray,
765
+ initial_state: np.ndarray,
766
+ previous_control: Optional[np.ndarray] = None,
767
+ state_ref: Optional[np.ndarray] = None,
768
+ control_ref: Optional[np.ndarray] = None,
769
+ max_iter: Optional[int] = None,
770
+ eps_abs: Optional[float] = None,
771
+ eps_rel: Optional[float] = None
772
+ ) -> Optional[np.ndarray]:
773
+ """Solve the mpc problem.
774
+
775
+ Args:
776
+ initial_state: Initial state (x_0).
777
+ previous_control: The control input in the previous
778
+ timestep (u_{-1}).
779
+ target_output: Reference output sequence Y_ref. Note that
780
+ this corresponds to the outputs from step 1 to step N
781
+ (y_1 to y_N), as the output at the current step 0
782
+ (y_0) cannot be influenced by future controls.
783
+ state_ref: Reference state for system linearization. This
784
+ corresponds to the states from step 0 to step N-1 (x_0
785
+ to x_{N-1}).
786
+ control_ref: Reference control for system
787
+ linearization. This corresponds to the controls from
788
+ step 0 to step N-1 (u_0 to u_{N-1}).
789
+ max_iter: Maximum iterations for OSQP solver.
790
+ eps_abs: Absolute convergence tolerance for OSQP solver.
791
+ eps_rel: Relative convergence tolerance for OSQP solver.
792
+
793
+ Returns:
794
+ The optimal control sequence U with shape (horizon,
795
+ n_control), corresponding to the controls from step 0 to
796
+ step N-1 (u_0 to u_{N-1}), or None if the problem is not
797
+ solved successfully.
798
+
799
+ """
800
+ use_cached_p = bool(self._qp_internal and
801
+ isinstance(self._system, AffineTimeInvariant))
802
+ use_cached_a = bool((not self._qp_constraint.modified) and
803
+ isinstance(self._system, AffineTimeInvariant))
804
+
805
+ previous_control = self._validate_previous_control(previous_control)
806
+
807
+ self._prepare_qp_matrices(
808
+ target_output, initial_state, previous_control,
809
+ state_ref, control_ref, use_cached_p)
810
+
811
+ self._prepare_constraints(
812
+ initial_state, previous_control, use_cached_a)
813
+
814
+ return self._setup_and_solve_osqp(
815
+ max_iter, eps_abs, eps_rel, control_ref,
816
+ use_cached_p, use_cached_a)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mpc-control
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A Python library for Model Predictive Control (MPC)
5
5
  Author-email: Zhen Ni <z.ni@hotmail.com>
6
6
  License-Expression: MIT
@@ -9,7 +9,7 @@ Project-URL: Issues, https://github.com/Zhen-Ni/mpc-control/issues
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Topic :: Scientific/Engineering
12
- Requires-Python: >=3.9
12
+ Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: numpy>=2.0
@@ -42,7 +42,7 @@ pip install mpc-control
42
42
  ```
43
43
 
44
44
  **Requirements:**
45
- - Python 3.9+ (Tested with Python 3.12)
45
+ - Python 3.12+
46
46
  - NumPy, SciPy, OSQP
47
47
 
48
48
  ## 🚀 Quick Start
@@ -51,7 +51,7 @@ Here is a basic example of how to define a system and solve an MPC problem:
51
51
 
52
52
  ```python
53
53
  import numpy as np
54
- import mpc_control as mpc
54
+ import mpc
55
55
 
56
56
  # 1. Define a discrete LTI system
57
57
  # x[n+1] = A x[n] + B u[n]
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mpc-control"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "A Python library for Model Predictive Control (MPC)"
9
9
  readme = "README.md"
10
- requires-python = ">=3.9"
10
+ requires-python = ">=3.12"
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [
@@ -76,6 +76,7 @@ class TestEKF(unittest.TestCase):
76
76
 
77
77
  def test_observe_state_noised(self):
78
78
  """State observer of a linear system with noise."""
79
+ np.random.seed(0)
79
80
  m1, m2 = 1.0, 1.5
80
81
  c1, c2 = 0.2, 0.3
81
82
  k1, k2 = 15.0, 20.0
@@ -137,10 +138,12 @@ class TestEKF(unittest.TestCase):
137
138
  x_true = np.array(x_true_list)
138
139
  x_pred = np.array(x_pred_list)
139
140
 
140
- self.assertTrue(abs(((x_true - x_pred)[-100:]) < 0.1).all())
141
+ self.assertTrue((abs((x_true - x_pred)[-100:]) < 0.1).all())
141
142
 
142
143
  def test_observe_nonlinear_state(self):
143
144
  """State observer of a nonlinear system."""
145
+ np.random.seed(0)
146
+
144
147
  def A_true(x, u):
145
148
  return np.array([0.5, 1.,
146
149
  0., 0.48 + np.sin(u[0]+0.02)]).reshape(2, 2)
@@ -256,6 +259,7 @@ class TestUKF(unittest.TestCase):
256
259
 
257
260
  def test_observe_state_noised(self):
258
261
  """State observer of a linear system with noise."""
262
+ np.random.seed(0)
259
263
  m1, m2 = 1.0, 1.5
260
264
  c1, c2 = 0.2, 0.3
261
265
  k1, k2 = 15.0, 20.0
@@ -317,10 +321,12 @@ class TestUKF(unittest.TestCase):
317
321
  x_true = np.array(x_true_list)
318
322
  x_pred = np.array(x_pred_list)
319
323
 
320
- self.assertTrue(abs(((x_true - x_pred)[-100:]) < 0.1).all())
324
+ self.assertTrue((abs((x_true - x_pred)[-100:]) < 0.1).all())
321
325
 
322
326
  def test_observe_nonlinear_state(self):
323
327
  """State observer of a nonlinear system."""
328
+ np.random.seed(0)
329
+
324
330
  def A_true(x, u):
325
331
  return np.array([0.5, 1., 0.,
326
332
  0.48 + np.sin(u[0]+0.02)]).reshape(2, 2)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes