Skip to content

Commit 399a64d

Browse files
Fix the spectral factorization (positive semidefiniteness) constraint for the power spectrum fit.
1 parent 49eb2b2 commit 399a64d

File tree

3 files changed

+46
-59
lines changed

3 files changed

+46
-59
lines changed

examples/7_uncertainty_characterization.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ def example_uncertainty_characterization():
4040

4141
# Fit overbounding stable and minimum-phase uncertainty weight system
4242
weight_left = dkpy.fit_uncertainty_weight(
43-
response_weight_left, omega, [4, 5], "diagonal"
43+
response_weight_left, omega, [8, 8], "diagonal"
4444
)
4545
weight_right = dkpy.fit_uncertainty_weight(
46-
response_weight_right, omega, [3, 5], "diagonal"
46+
response_weight_right, omega, [8, 8], "diagonal"
4747
)
4848

4949
# Plot: Magnitude response of nominal and off-nominal systems

src/dkpy/lti_system_fit.py

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def fit_spectral_factor_mimo_ct(
2525
tol_bisection: float = 1e-5,
2626
max_iter_bisection: int = 100,
2727
max_iter_bisection_init: int = 15,
28-
omega_power_constraint: Optional[np.ndarray] = None,
28+
nbr_power_constraint: int = 500,
2929
) -> control.StateSpace:
3030
"""Fit a stable and minimum-phase biproper MIMO spectral factor to power spectrum.
3131
@@ -52,8 +52,8 @@ def fit_spectral_factor_mimo_ct(
5252
Maximum number of iterations for the bisection algorithm.
5353
max_iter_bisection_init : int
5454
Maximum number of iterations for the bisection algorithm initialization.
55-
omega_power_constraint : Optional[np.ndarray]
56-
Angular frequencies to enforce non-negativity of power spectrum.
55+
nbr_power_constraint : Optional[np.ndarray]
56+
Number of frequencies to enforce non-negativity of power spectrum.
5757
5858
Returns
5959
-------
@@ -81,11 +81,6 @@ def fit_spectral_factor_mimo_ct(
8181
# Pre-warp frequency (continuous- to discrete-time) with bilinear transformation
8282
alpha = np.sqrt(omega[0] * omega[-1])
8383
theta = 2 * np.atan(omega / alpha)
84-
theta_power_constraint = (
85-
2 * np.atan(omega_power_constraint / alpha)
86-
if omega_power_constraint is not None
87-
else None
88-
)
8984

9085
# Parse frequency-dependent weight
9186
if weight is None:
@@ -103,7 +98,7 @@ def fit_spectral_factor_mimo_ct(
10398
tol_bisection,
10499
max_iter_bisection,
105100
max_iter_bisection_init,
106-
theta_power_constraint,
101+
nbr_power_constraint,
107102
)
108103

109104
# Discrete-time spectral factor fit model
@@ -133,7 +128,7 @@ def fit_power_mimo_dt(
133128
tol_bisection: float = 1e-5,
134129
max_iter_bisection: int = 100,
135130
max_iter_bisection_init: int = 15,
136-
theta_power_constraint: Optional[np.ndarray] = None,
131+
nbr_power_constraint: int = 500,
137132
) -> Tuple[np.ndarray, np.ndarray]:
138133
"""Fit a biproper discrete-time MIMO power spectrum transfer function.
139134
@@ -160,9 +155,8 @@ def fit_power_mimo_dt(
160155
Maximum number of iterations for the bisection algorithm.
161156
max_iter_bisection_init : int
162157
Maximum number of iterations for the bisection algorithm initialization.
163-
theta_power_constraint : Optional[np.ndarray]
164-
Discrete-time frequencies to enforce non-negativity of power spectrum. The
165-
frequencies range from [0, pi].
158+
nbr_power_constraint : Optional[np.ndarray]
159+
Number of frequencies to enforce non-negativity of power spectrum.
166160
167161
Returns
168162
-------
@@ -177,9 +171,6 @@ def fit_power_mimo_dt(
177171
# Parse arguments
178172
solver_params = _parse_solver_param(solver_params)
179173
weight = np.ones_like(theta) if weight is None else weight
180-
theta_power_constraint = (
181-
theta if theta_power_constraint is None else theta_power_constraint
182-
)
183174

184175
# Auxiliary parameters
185176
nbr_signals = power_fit.shape[1]
@@ -210,7 +201,7 @@ def fit_power_mimo_dt(
210201
theta,
211202
order,
212203
weight,
213-
theta_power_constraint,
204+
nbr_power_constraint,
214205
)
215206

216207
# Optimization objective
@@ -253,7 +244,7 @@ def _generate_constraints_power_mimo_dt(
253244
theta: np.ndarray,
254245
order: int,
255246
weight: np.ndarray,
256-
theta_power_constraint: np.ndarray,
247+
nbr_power_constraint: int,
257248
) -> List[cvxpy.Constraint]:
258249
"""Generate the constraints for the fit of discrete-time MIMO power spectrums.
259250
@@ -281,9 +272,8 @@ def _generate_constraints_power_mimo_dt(
281272
Order of the power spectrum numerator and denominator polynomials.
282273
weight : np.ndarray,
283274
Frequency-dependent weight for fit accuracy.
284-
theta_power_constraint : Optional[np.ndarray]
285-
Discrete-time frequencies to enforce positive semidefiniteness of power
286-
spectrum. The frequencies range from [0, pi].
275+
nbr_power_constraint : Optional[np.ndarray]
276+
Number of frequencies to enforce positive semidefiniteness of power spectrum.
287277
288278
Returns
289279
-------
@@ -306,7 +296,7 @@ def _generate_constraints_power_mimo_dt(
306296

307297
# Power spectrum positive definiteness constraints
308298
constraint_power_psd = _construct_power_psd_constraints_mimo_dt(
309-
num_coef_sym_list, num_coef_skew_list, den_coef, theta_power_constraint, order
299+
num_coef_sym_list, num_coef_skew_list, den_coef, order, nbr_power_constraint
310300
)
311301

312302
# Numerator variable structure constraints
@@ -472,8 +462,8 @@ def _construct_power_psd_constraints_mimo_dt(
472462
num_coef_sym_list: List[cvxpy.Variable],
473463
num_coef_skew_list: List[cvxpy.Variable],
474464
den_coef: cvxpy.Variable,
475-
theta: np.ndarray,
476465
order: int,
466+
nbr_theta: int,
477467
) -> List[cvxpy.Constraint]:
478468
"""Construct the power spectrum positive semidefiniteness constraints.
479469
@@ -503,6 +493,9 @@ def _construct_power_psd_constraints_mimo_dt(
503493
# Parameters
504494
nbr_signals = num_coef_sym_list[0].shape[0]
505495

496+
# Frequency points
497+
theta = np.linspace(0, np.pi, nbr_theta)
498+
506499
# Augmented variables
507500
num_coef_sym_stack = cvxpy.vstack(num_coef_sym_list)
508501
num_coef_skew_stack = cvxpy.vstack(num_coef_skew_list)
@@ -704,7 +697,7 @@ def fit_magnitude_siso_ct(
704697
tol_bisection: float = 1e-3,
705698
max_iter_bisection: int = 500,
706699
max_iter_bisection_init: int = 15,
707-
omega_power_constraint: Optional[np.ndarray] = None,
700+
nbr_power_constraint: int = 500,
708701
) -> control.StateSpace:
709702
"""Fit a stable and minimum-phase biproper SISO system to magnitude.
710703
@@ -734,8 +727,8 @@ def fit_magnitude_siso_ct(
734727
Maximum allowable number of iterations in the bisection algorithm.
735728
max_iter_bisection_init : int
736729
Maximum number of iterations for the bisection algorithm initialization.
737-
omega_power_constraint : Optional[np.ndarray]
738-
Angular frequencies to enforce non-negativity of power spectrum.
730+
nbr_power_constraint : Optional[np.ndarray]
731+
Number of frequencies to enforce non-negativity of power spectrum.
739732
740733
Returns
741734
-------
@@ -774,11 +767,6 @@ def fit_magnitude_siso_ct(
774767
# Pre-warp frequency (continuous- to discrete-time) with bilinear transformation
775768
alpha = np.sqrt(omega[0] * omega[-1])
776769
theta = 2 * np.atan(omega / alpha)
777-
theta_power_constraint = (
778-
2 * np.atan(omega_power_constraint / alpha)
779-
if omega_power_constraint is not None
780-
else None
781-
)
782770

783771
# Parse frequency-dependent weight
784772
if weight is None:
@@ -796,7 +784,7 @@ def fit_magnitude_siso_ct(
796784
tol_bisection,
797785
max_iter_bisection,
798786
max_iter_bisection_init,
799-
theta_power_constraint,
787+
nbr_power_constraint,
800788
)
801789

802790
# Discrete-time spectral factor model
@@ -822,7 +810,7 @@ def fit_power_siso_dt(
822810
tol_bisection: float = 1e-5,
823811
max_iter_bisection: int = 100,
824812
max_iter_bisection_init: int = 15,
825-
theta_power_constraint: Optional[np.ndarray] = None,
813+
nbr_power_constraint: int = 500,
826814
) -> Tuple[np.ndarray, np.ndarray]:
827815
"""Fit a biproper discrete-time SISO power spectrum transfer function.
828816
@@ -849,9 +837,8 @@ def fit_power_siso_dt(
849837
Maximum number of iterations for the bisection algorithm.
850838
max_iter_bisection_init : int
851839
Maximum number of iterations for the bisection algorithm initialization.
852-
theta_power_constraint : Optional[np.ndarray]
853-
Discrete-time frequencies to enforce non-negativity of power spectrum. The
854-
frequencies range from [0, pi].
840+
nbr_power_constraint : Optional[np.ndarray]
841+
Number of frequencies to enforce non-negativity of power spectrum.
855842
856843
Returns
857844
-------
@@ -882,9 +869,6 @@ def fit_power_siso_dt(
882869
# Parse arguments
883870
solver_params = _parse_solver_param(solver_params)
884871
weight = np.ones_like(theta) if weight is None else weight
885-
theta_power_constraint = (
886-
theta if theta_power_constraint is None else theta_power_constraint
887-
)
888872

889873
# Optimization variables and parameters
890874
num_coef = cvxpy.Variable(shape=order + 1, name="num_coef")
@@ -902,7 +886,7 @@ def fit_power_siso_dt(
902886
theta,
903887
order,
904888
weight,
905-
theta_power_constraint,
889+
nbr_power_constraint,
906890
)
907891

908892
# Optimization problem
@@ -945,7 +929,7 @@ def _generate_constraints_power_siso_dt(
945929
theta: np.ndarray,
946930
order: int,
947931
weight: np.ndarray,
948-
theta_power_constraints: np.ndarray,
932+
nbr_power_constraints: int,
949933
) -> List[cvxpy.Constraint]:
950934
"""Generate the constraints for the discrete-time SISO power spectrums.
951935
@@ -969,9 +953,8 @@ def _generate_constraints_power_siso_dt(
969953
Order of the power spectrum model.
970954
weight : np.ndarray
971955
Frequency-dependent weight for fit accuracy.
972-
theta_power_constraint : Optional[np.ndarray]
973-
Discrete-time frequencies to enforce non-negativity of power spectrum. The
974-
frequencies range from [0, pi].
956+
nbr_power_constraint : int
957+
Discrete-time frequencies to enforce non-negativity of power spectrum.
975958
976959
Returns
977960
-------
@@ -996,7 +979,7 @@ def _generate_constraints_power_siso_dt(
996979
num_coef,
997980
den_coef,
998981
order,
999-
theta_power_constraints,
982+
nbr_power_constraints,
1000983
)
1001984

1002985
# Normalization of the transfer function coefficients
@@ -1135,7 +1118,10 @@ def _construct_bound_constraints_siso_dt(
11351118

11361119

11371120
def _construct_spectral_factorization_constraint_siso_dt(
1138-
num_coef: cvxpy.Variable, den_coef: cvxpy.Variable, order: int, theta: np.ndarray
1121+
num_coef: cvxpy.Variable,
1122+
den_coef: cvxpy.Variable,
1123+
order: int,
1124+
nbr_theta: np.ndarray,
11391125
) -> List[cvxpy.Constraint]:
11401126
"""Construct power spectrum spectral factorization constraints.
11411127
@@ -1150,6 +1136,8 @@ def _construct_spectral_factorization_constraint_siso_dt(
11501136
Power spectrum denominator coefficient variable.
11511137
order : int
11521138
Order of the power spectrum model.
1139+
nbr_theta : int
1140+
Number of frequencies to enforce constraint.
11531141
11541142
Returns
11551143
-------
@@ -1158,6 +1146,9 @@ def _construct_spectral_factorization_constraint_siso_dt(
11581146
constraints.
11591147
"""
11601148

1149+
# Frequencies to enforce power spectrum non-negativity
1150+
theta = np.linspace(0, np.pi, nbr_theta)
1151+
11611152
constraint_spectral_factorization_list = []
11621153
for theta_idx in theta:
11631154
# Constraint data matrices

src/dkpy/uncertainty_characterization.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"plot_magnitude_response_uncertainty_weight",
1313
]
1414

15-
import warnings
1615

1716
import control
1817
import numpy as np
@@ -787,7 +786,7 @@ def fit_uncertainty_weight(
787786
tol_bisection: float = 1e-3,
788787
max_iter_bisection: int = 500,
789788
max_iter_bisection_init: int = 15,
790-
nbr_omega_power_constraint: int = 500,
789+
nbr_power_constraint: int = 500,
791790
) -> control.StateSpace:
792791
"""Fit an overbounding stable and minimum-phase uncertainty weight.
793792
@@ -888,9 +887,6 @@ def fit_uncertainty_weight(
888887
f"`{weight_structure}`."
889888
)
890889

891-
# Thing
892-
omega_power_constraint = np.linspace(0, np.pi, nbr_omega_power_constraint)
893-
894890
# Fit uncertainty weight
895891
uncertainty_weight = fit_uncertainty_weight_method(
896892
complex_uncertainty_weight,
@@ -901,7 +897,7 @@ def fit_uncertainty_weight(
901897
tol_bisection,
902898
max_iter_bisection,
903899
max_iter_bisection_init,
904-
omega_power_constraint,
900+
nbr_power_constraint,
905901
)
906902

907903
return uncertainty_weight
@@ -916,7 +912,7 @@ def _fit_uncertainty_weight_scalar(
916912
tol_bisection: float = 1e-3,
917913
max_iter_bisection: int = 500,
918914
max_iter_bisection_init: int = 15,
919-
omega_power_constraint: Optional[np.ndarray] = None,
915+
nbr_power_constraint: int = 500,
920916
) -> control.StateSpace:
921917
# Auxiliary parameters
922918
nbr_signals = complex_uncertainty_weight.shape[1]
@@ -938,7 +934,7 @@ def _fit_uncertainty_weight_scalar(
938934
tol_bisection=tol_bisection,
939935
max_iter_bisection=max_iter_bisection,
940936
max_iter_bisection_init=max_iter_bisection_init,
941-
omega_power_constraint=omega_power_constraint,
937+
nbr_power_constraint=nbr_power_constraint,
942938
)
943939

944940
# Duplicate the scalar uncertainty weight along the diagonal
@@ -956,7 +952,7 @@ def _fit_uncertainty_weight_diagonal(
956952
tol_bisection: float = 1e-3,
957953
max_iter_bisection: int = 500,
958954
max_iter_bisection_init: int = 15,
959-
omega_power_constraint: Optional[np.ndarray] = None,
955+
nbr_power_constraint: int = 500,
960956
):
961957
# Auxiliary parameters
962958
nbr_signals = complex_uncertainty_weight.shape[1]
@@ -969,7 +965,6 @@ def _fit_uncertainty_weight_diagonal(
969965
for idx in range(nbr_signals):
970966
# Compute the magnitude of the diagonal uncertainty weight element
971967
magnitude_uncertainty_weight = np.abs(complex_uncertainty_weight[:, idx, idx])
972-
print(magnitude_uncertainty_weight)
973968
order_idx = order_list[idx]
974969

975970
# Fit the diagonal uncertainty weight element
@@ -984,7 +979,7 @@ def _fit_uncertainty_weight_diagonal(
984979
tol_bisection=tol_bisection,
985980
max_iter_bisection=max_iter_bisection,
986981
max_iter_bisection_init=max_iter_bisection_init,
987-
omega_power_constraint=omega_power_constraint,
982+
nbr_power_constraint=nbr_power_constraint,
988983
)
989984
uncertainty_weight_list.append(uncertainty_weight_diagonal)
990985

@@ -994,6 +989,7 @@ def _fit_uncertainty_weight_diagonal(
994989
return uncertainty_weight
995990

996991

992+
# TODO: Fit spectral factorization constraint generation
997993
def _fit_uncertainty_weight_full(
998994
complex_uncertainty_weight: np.ndarray,
999995
omega: np.ndarray,

0 commit comments

Comments
 (0)