From b72f6855335690c5056165917b0a598b116de929 Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 14 May 2026 11:50:58 -0400 Subject: [PATCH 1/9] Vectorize uncertainty residual computation. --- src/dkpy/uncertainty_characterization.py | 413 ++++++++------------- tests/test_uncertainty_characterization.py | 83 ++--- 2 files changed, 186 insertions(+), 310 deletions(-) diff --git a/src/dkpy/uncertainty_characterization.py b/src/dkpy/uncertainty_characterization.py index f9787cd..52cc112 100644 --- a/src/dkpy/uncertainty_characterization.py +++ b/src/dkpy/uncertainty_characterization.py @@ -20,27 +20,29 @@ import scipy from matplotlib import pyplot as plt -from typing import List, Optional, Union, Tuple, Dict, Callable, Set, Any +from typing import List, Optional, Union, Tuple, Dict, Any, Literal from matplotlib.figure import Figure from matplotlib.axes import Axes from matplotlib.legend import Legend from . import utilities +UncertaintyModelId = Literal[ + "additive", + "multiplicative_input", + "multiplicative_output", + "inverse_additive", + "inverse_multiplicative_input", + "inverse_multiplicative_output", +] + def compute_uncertainty_residual_response( complex_response_nom: Union[np.ndarray, control.FrequencyResponseData], complex_response_offnom_list: Union[np.ndarray, control.FrequencyResponseList], - uncertainty_model: Union[str, List[str], Set[str]] = { - "additive", - "multiplicative_input", - "multiplicative_output", - "inverse_additive", - "inverse_multiplicative_input", - "inverse_multiplicative_output", - }, + uncertainty_model: Union[UncertaintyModelId, List[UncertaintyModelId]], tol_residual_existence: float = 1e-12, -) -> Dict[str, np.ndarray]: +) -> Dict[UncertaintyModelId, np.ndarray]: """Compute the residual response of unstructured uncertainty models. Parameters @@ -49,7 +51,7 @@ def compute_uncertainty_residual_response( Frequency response of the nominal system. complex_response_offnom_list : Union[np.ndarray, control.FrequencyResponseList] Frequency response of the off-nominal system. - uncertainty_model : Union[str, List[str], Set[str]] + uncertainty_model : Union[UncertaintyModelId, List[UncertaintyModelId]] Uncertainty model identifiers to compute the residual response. tol_residual_existence : float Tolerance for the existence of an uncertainty residual. @@ -98,202 +100,82 @@ def compute_uncertainty_residual_response( # Uncertainty residual response dictionary complex_response_residual_dict = {} - # Check uncertainty model identifiers - uncertainty_model = set(uncertainty_model) - valid_uncertainty_model = { - "additive", - "multiplicative_input", - "multiplicative_output", - "inverse_additive", - "inverse_multiplicative_input", - "inverse_multiplicative_output", + compute_residual_dispatcher = { + "additive": _compute_residual_additive, + "multiplicative_input": _compute_residual_multiplicative_input, + "multiplicative_output": _compute_residual_multiplicative_output, + "inverse_additive": _compute_residual_inverse_additive, + "inverse_multiplicative_input": _compute_residual_inverse_multiplicative_input, + "inverse_multiplicative_output": _compute_residual_inverse_multiplicative_output, } - if not uncertainty_model.issubset(valid_uncertainty_model): - raise ValueError( - "The uncertainty model identifiers provided in `uncertainty_model` do not " - "all correspond to valid uncertainty models. In particular, " - f"{uncertainty_model.difference(valid_uncertainty_model)} are not valid " - "uncertainty model identifiers. The identifiers are: " - '"additive": Additive uncertainty, ' - '"multiplicative_input": Multiplicative input uncertainty, ' - '"multiplicative_output": Multiplicative output uncertainty, ' - '"inverse_additive": Inverse additive uncertainty, ' - '"inverse_multiplicative_input": Inverse multiplicative input uncertainty, ' - '"inverse_multiplicative_output": Inverse multiplicative output uncertainty.' - ) - - # Additive uncertainty residual response - if "additive" in uncertainty_model: - complex_response_residual_list = _compute_uncertainty_residual_response( - complex_response_nom, - complex_response_offnom_list, - _compute_uncertainty_residual_additive_freq, - tol_residual_existence, - ) - complex_response_residual_dict["additive"] = complex_response_residual_list - - # Multiplicative input uncertainty residual response - if "multiplicative_input" in uncertainty_model: - complex_response_residual_list = _compute_uncertainty_residual_response( - complex_response_nom, - complex_response_offnom_list, - _compute_uncertainty_residual_multiplicative_input_freq, - tol_residual_existence, - ) - complex_response_residual_dict["multiplicative_input"] = ( - complex_response_residual_list - ) - - # Multiplicative output uncertainty residual response - if "multiplicative_output" in uncertainty_model: - complex_response_residual_list = _compute_uncertainty_residual_response( - complex_response_nom, - complex_response_offnom_list, - _compute_uncertainty_residual_multiplicative_output_freq, - tol_residual_existence, - ) - complex_response_residual_dict["multiplicative_output"] = ( - complex_response_residual_list - ) - - # Inverse additive uncertainty residual response - if "inverse_additive" in uncertainty_model: - complex_response_residual_list = _compute_uncertainty_residual_response( - complex_response_nom, - complex_response_offnom_list, - _compute_uncertainty_residual_inverse_additive_freq, - tol_residual_existence, - ) - complex_response_residual_dict["inverse_additive"] = ( - complex_response_residual_list - ) - # Inverse multiplicative input uncertainty residual response - if "inverse_multiplicative_input" in uncertainty_model: - complex_response_residual_list = _compute_uncertainty_residual_response( - complex_response_nom, - complex_response_offnom_list, - _compute_uncertainty_residual_inverse_multiplicative_input_freq, - tol_residual_existence, - ) - complex_response_residual_dict["inverse_multiplicative_input"] = ( - complex_response_residual_list - ) - - # Inverse multiplicative output uncertainty residual response - if "inverse_multiplicative_output" in uncertainty_model: - complex_response_residual_list = _compute_uncertainty_residual_response( + for model in uncertainty_model: + try: + compute_residual_model = compute_residual_dispatcher[model] + except KeyError: + raise KeyError( + "The uncertainty model identifier must be `additive`, " + "`multiplicative_input`, `multiplicative_output`, `inverse_additive` " + "`inverse_multiplicative_input`, or `inverse_multiplicative_output` " + f"(got `{model}`)." + ) + complex_response_residual_list = compute_residual_model( complex_response_nom, complex_response_offnom_list, - _compute_uncertainty_residual_inverse_multiplicative_output_freq, tol_residual_existence, ) - complex_response_residual_dict["inverse_multiplicative_output"] = ( - complex_response_residual_list - ) + complex_response_residual_dict[model] = complex_response_residual_list return complex_response_residual_dict -def _compute_uncertainty_residual_response( - complex_response_nom: np.ndarray, - complex_response_offnom_list: np.ndarray, - compute_uncertainty_residual_freq: Callable, - tol_residual_existence: float, -) -> np.ndarray: - """Compute the uncertainty residual response for a given uncertainty model. - - Parameters - ---------- - complex_response_nom : np.ndarray - Frequency response of the nominal system. - complex_response_offnom_list : np.ndarray - Frequency response of the off-nominal system. - compute_uncertainty_residual_freq : Callable, - Uncertainty residual computation function at a given frequency. - tol_residual_existence : float - Tolerance for the existence of an uncertainty residual. - - Returns - ------- - np.ndarray - Uncertainty residual response of the given uncertainty model. - """ - - # Frequency response parameters - num_offnom = complex_response_offnom_list.shape[0] - num_frequency = complex_response_offnom_list.shape[1] - - complex_response_residual_list = [] - for idx_offnom in range(num_offnom): - complex_response_residual = [] - complex_response_offnom = complex_response_offnom_list[idx_offnom, :, :, :] - for idx_freq in range(num_frequency): - complex_response_nom_freq = complex_response_nom[idx_freq, :, :] - complex_response_offnom_freq = complex_response_offnom[idx_freq, :, :] - complex_response_residual_freq = compute_uncertainty_residual_freq( - complex_response_nom_freq, - complex_response_offnom_freq, - tol_residual_existence, - ) - complex_response_residual.append(complex_response_residual_freq) - complex_response_residual = np.array(complex_response_residual, dtype=complex) - complex_response_residual_list.append(complex_response_residual) - complex_response_residual_list = np.array(complex_response_residual_list) - - return complex_response_residual_list - - -def _compute_uncertainty_residual_additive_freq( - complex_response_nom_freq: np.ndarray, - complex_response_offnom_freq: np.ndarray, +def _compute_residual_additive( + complex_nominal: np.ndarray, + complex_offnominal: np.ndarray, tol_residual_existence: Optional[float] = None, ) -> np.ndarray: - """Compute the additive uncertainty residual at a frequency. + """Compute the additive uncertainty residual frequency responses. Parameters ---------- - complex_response_nom_freq : np.ndarray - Nominal frequency response matrix evaluated at a given frequency. - complex_response_offnom_freq : np.ndarray - Frequency response matrix of a single off-nominal system evaluated at a given - frequency. + complex_nominal : np.ndarray + Nominal model frequency response. + complex_offnominal : np.ndarray + Off-nominal model frequency responses. tol_residual_existence : float - Tolerance for the existence of a multiplicative input uncertainty residual. + Tolerance for the existence of an uncertainty residual. Returns ------- np.ndarray - Additive uncertainty residual at a given frequency. + Additive uncertainty residual frequency response for all off-nominal models. """ - complex_response_residual_freq = ( - complex_response_offnom_freq - complex_response_nom_freq - ) + complex_residual = complex_offnominal - complex_nominal - return complex_response_residual_freq + return complex_residual -def _compute_uncertainty_residual_multiplicative_input_freq( - complex_response_nom_freq: np.ndarray, - complex_response_offnom_freq: np.ndarray, +def _compute_residual_multiplicative_input( + complex_nominal: np.ndarray, + complex_offnominal: np.ndarray, tol_residual_existence: float = 1e-8, ) -> np.ndarray: - """Compute the multiplicative input uncertainty residual at a frequency. + """Compute the multiplicative input uncertainty residual frequency responses. Parameters ---------- - complex_response_nom_freq : np.ndarray - Nominal frequency response matrix evaluated at a given frequency. - complex_response_offnom_freq : np.ndarray - Frequency response matrix of a single off-nominal system evaluated at a given - frequency. + complex_response_nom : np.ndarray + Nominal model frequency response. + complex_response_offnom : np.ndarray + Off-nominal model frequency responses. tol_residual_existence : float - Tolerance for the existence of a multiplicative input uncertainty residual. + Tolerance for the existence of an uncertainty residual. Returns ------- np.ndarray - Multiplicative input uncertainty residual at a given frequency. + Multiplicative input uncertainty residual frequency response for all + off-nominal models. Raises ------ @@ -303,19 +185,19 @@ def _compute_uncertainty_residual_multiplicative_input_freq( solution. """ - num_inputs = complex_response_nom_freq.shape[1] - num_outputs = complex_response_nom_freq.shape[0] + nbr_inputs = complex_nominal.shape[-1] + nbr_outputs = complex_nominal.shape[-2] - A = complex_response_nom_freq - B = complex_response_offnom_freq - complex_response_nom_freq - X, residues_lstsq, _, _ = scipy.linalg.lstsq(A, B) - complex_response_residual_freq = X + a = complex_nominal + b = complex_offnominal - complex_nominal + x, residues_lstsq, _, _ = scipy.linalg.lstsq(a, b) + complex_residual = x - if num_inputs >= num_outputs: - return complex_response_residual_freq + if nbr_inputs >= nbr_outputs: + return complex_residual else: if np.all(residues_lstsq <= tol_residual_existence): - return complex_response_residual_freq + return complex_residual else: raise ValueError( "A multiplicative input uncertainty residual does not exist for the " @@ -327,27 +209,27 @@ def _compute_uncertainty_residual_multiplicative_input_freq( ) -def _compute_uncertainty_residual_multiplicative_output_freq( - complex_response_nom_freq: np.ndarray, - complex_response_offnom_freq: np.ndarray, +def _compute_residual_multiplicative_output( + complex_nominal: np.ndarray, + complex_offnominal: np.ndarray, tol_residual_existence: float = 1e-8, ) -> np.ndarray: - """Compute the multiplicative output uncertainty residual at a frequency. + """Compute the multiplicative output uncertainty residual frequency responses. Parameters ---------- - complex_response_nom_freq : np.ndarray - Nominal frequency response matrix evaluated at a given frequency. - complex_response_offnom_freq : np.ndarray - Frequency response matrix of a single off-nominal system evaluated at a given - frequency. + complex_nominal : np.ndarray + Nominal model frequency response. + complex_offnominal : np.ndarray + Off-nominal model frequency responses. tol_residual_existence : float - Tolerance for the existence of a multiplicative input uncertainty residual. + Tolerance for the existence of an uncertainty residual. Returns ------- np.ndarray - Multiplicative output uncertainty residual at a given frequency. + Multiplicative output uncertainty residual frequency response for all + off-nominal models. Raises ------ @@ -357,19 +239,19 @@ def _compute_uncertainty_residual_multiplicative_output_freq( solution. """ - num_inputs = complex_response_nom_freq.shape[1] - num_outputs = complex_response_nom_freq.shape[0] + nbr_inputs = complex_nominal.shape[-1] + nbr_outputs = complex_nominal.shape[-2] - A = complex_response_nom_freq.T - B = complex_response_offnom_freq.T - complex_response_nom_freq.T - X, residues_lstsq, _, _ = scipy.linalg.lstsq(A, B) - complex_response_residual_freq = X.T + a = np.moveaxis(complex_nominal, -1, -2) + b = np.moveaxis(complex_offnominal - complex_nominal, -1, -2) + x, residues_lstsq, _, _ = scipy.linalg.lstsq(a, b) + complex_residual = np.moveaxis(x, -1, -2) - if num_inputs <= num_outputs: - return complex_response_residual_freq + if nbr_inputs <= nbr_outputs: + return complex_residual else: if np.all(residues_lstsq <= tol_residual_existence): - return complex_response_residual_freq + return complex_residual else: raise ValueError( "A multiplicative output uncertainty residual does not exist for the " @@ -381,27 +263,27 @@ def _compute_uncertainty_residual_multiplicative_output_freq( ) -def _compute_uncertainty_residual_inverse_additive_freq( - complex_response_nom_freq: np.ndarray, - complex_response_offnom_freq: np.ndarray, +def _compute_residual_inverse_additive( + complex_nominal: np.ndarray, + complex_offnominal: np.ndarray, tol_residual_existence: float = 1e-8, ) -> np.ndarray: - """Compute the inverse additive uncertainty residual at a frequency. + """Compute the inverse additive uncertainty residual frequency responses. Parameters ---------- - complex_response_nom_freq : np.ndarray - Nominal frequency response matrix evaluated at a given frequency. - complex_response_offnom_freq : np.ndarray - Frequency response matrix of a single off-nominal system evaluated at a given - frequency. + complex_nominal : np.ndarray + Nominal model frequency response. + complex_offnominal : np.ndarray + Off-nominal model frequency responses. tol_residual_existence : float - Tolerance for the existence of a inverse additive uncertainty residual. + Tolerance for the existence of an uncertainty residual. Returns ------- np.ndarray - Inverse additive uncertainty residual at a given frequency. + Inverse additive uncertainty residual frequency response for all + off-nominal models. Raises ------ @@ -411,24 +293,24 @@ def _compute_uncertainty_residual_inverse_additive_freq( solution. """ - num_inputs = complex_response_nom_freq.shape[1] - num_outputs = complex_response_nom_freq.shape[0] + nbr_inputs = complex_nominal.shape[-1] + nbr_outputs = complex_nominal.shape[-2] - A1 = complex_response_offnom_freq - B1 = complex_response_offnom_freq - complex_response_nom_freq - Y, residues_lstsq_1, _, _ = scipy.linalg.lstsq(A1, B1) - A2 = complex_response_nom_freq.T - B2 = Y.T - X, residues_lstsq_2, _, _ = scipy.linalg.lstsq(A2, B2) - complex_response_residual_freq = X.T + a1 = complex_offnominal + b1 = complex_offnominal - complex_nominal + y, residues_lstsq_1, _, _ = scipy.linalg.lstsq(a1, b1) + a2 = np.moveaxis(complex_nominal, -1, -2) + b2 = np.moveaxis(y, -1, -2) + x, residues_lstsq_2, _, _ = scipy.linalg.lstsq(a2, b2) + complex_residual = np.moveaxis(x, -1, -2) - if num_inputs == num_outputs: - return complex_response_residual_freq + if nbr_inputs == nbr_outputs: + return complex_residual else: if np.all(residues_lstsq_1 <= tol_residual_existence) and np.all( residues_lstsq_2 <= tol_residual_existence ): - return complex_response_residual_freq + return complex_residual else: raise ValueError( "An inverse additive uncertainty residual does not exist for the " @@ -440,28 +322,27 @@ def _compute_uncertainty_residual_inverse_additive_freq( ) -def _compute_uncertainty_residual_inverse_multiplicative_input_freq( - complex_response_nom_freq: np.ndarray, - complex_response_offnom_freq: np.ndarray, +def _compute_residual_inverse_multiplicative_input( + complex_nominal: np.ndarray, + complex_offnominal: np.ndarray, tol_residual_existence: float = 1e-8, ) -> np.ndarray: - """Compute the inverse multiplicative input uncertainty residual at a frequency. + """Compute the inverse multiplicative input uncertainty residual frequency responses. Parameters ---------- - complex_response_nom_freq: np.ndarray - Nominal frequency response matrix evaluated at a given frequency. - complex_response_offnom_freq : np.ndarray - Frequency response matrix of a single off-nominal system evaluated at a given - frequency. + complex_nominal : np.ndarray + Nominal model frequency response. + complex_offnominal : np.ndarray + Off-nominal model frequency responses. tol_residual_existence : float - Tolerance for the existence of an inverse multiplicative input uncertainty - residual. + Tolerance for the existence of an uncertainty residual. Returns ------- np.ndarray - Inverse multiplicative input uncertainty residual at a given frequency. + Inverse multiplicative input uncertainty residual frequency response for all + off-nominal models. Raises ------ @@ -471,19 +352,19 @@ def _compute_uncertainty_residual_inverse_multiplicative_input_freq( a solution. """ - num_inputs = complex_response_nom_freq.shape[1] - num_outputs = complex_response_nom_freq.shape[0] + nbr_inputs = complex_nominal.shape[-1] + nbr_outputs = complex_nominal.shape[-2] - A = complex_response_offnom_freq - B = complex_response_offnom_freq - complex_response_nom_freq - X, residues_lstsq, _, _ = scipy.linalg.lstsq(A, B) - complex_response_residual_freq = X + a = complex_offnominal + b = complex_offnominal - complex_nominal + x, residues_lstsq, _, _ = scipy.linalg.lstsq(a, b) + complex_residual = x - if num_inputs >= num_outputs: - return complex_response_residual_freq + if nbr_inputs >= nbr_outputs: + return complex_residual else: if np.all(residues_lstsq <= tol_residual_existence): - return complex_response_residual_freq + return complex_residual else: raise ValueError( "An inverse multiplicative input uncertainty residual does not exist " @@ -495,27 +376,27 @@ def _compute_uncertainty_residual_inverse_multiplicative_input_freq( ) -def _compute_uncertainty_residual_inverse_multiplicative_output_freq( - complex_response_nom_freq: np.ndarray, - complex_response_offnom_freq: np.ndarray, +def _compute_residual_inverse_multiplicative_output( + complex_nominal: np.ndarray, + complex_offnominal: np.ndarray, tol_residual_existence: float = 1e-8, ) -> np.ndarray: - """Compute the inverse multiplicative output uncertainty residual at a frequency. + """Compute the inverse multiplicative output uncertainty residual frequency responses. Parameters ---------- - complex_response_nom_freq : np.ndarray - Nominal frequency response matrix evaluated at a given frequency. - complex_response_offnom_freq : np.ndarray - Frequency response matrix of a single off-nominal system evaluated at a given - frequency. + complex_nominal : np.ndarray + Nominal model frequency response. + complex_offnominal : np.ndarray + Off-nominal model frequency responses. tol_residual_existence : float - Tolerance for the existence of a multiplicative input uncertainty residual. + Tolerance for the existence of an uncertainty residual. Returns ------- np.ndarray - Inverse multiplicative output uncertainty residual at a given frequency. + Inverse multiplicative output uncertainty residual frequency response for all + off-nominal models. Raises ------ @@ -525,19 +406,19 @@ def _compute_uncertainty_residual_inverse_multiplicative_output_freq( a solution. """ - num_inputs = complex_response_nom_freq.shape[1] - num_outputs = complex_response_nom_freq.shape[0] + nbr_inputs = complex_nominal.shape[-1] + nbr_outputs = complex_nominal.shape[-2] - A = complex_response_offnom_freq.T - B = complex_response_offnom_freq.T - complex_response_nom_freq.T - X, residues_lstsq, _, _ = scipy.linalg.lstsq(A, B) - complex_response_residual_freq = X.T + a = np.moveaxis(complex_offnominal, -1, -2) + b = np.moveaxis(complex_offnominal - complex_nominal, -1, -2) + x, residues_lstsq, _, _ = scipy.linalg.lstsq(a, b) + complex_residual = np.moveaxis(x, -1, -2) - if num_inputs <= num_outputs: - return complex_response_residual_freq + if nbr_inputs <= nbr_outputs: + return complex_residual else: if np.all(residues_lstsq <= tol_residual_existence): - return complex_response_residual_freq + return complex_residual else: raise ValueError( "An inverse multiplicative output uncertainty residual does not exist " diff --git a/tests/test_uncertainty_characterization.py b/tests/test_uncertainty_characterization.py index 390053c..915b32b 100644 --- a/tests/test_uncertainty_characterization.py +++ b/tests/test_uncertainty_characterization.py @@ -84,21 +84,14 @@ def test_compute_uncertainty_residual_response( """Regression test :func:`compute_uncertainty_residual_response`.""" # Frequency response of systems - frequency_response_nom = control.frequency_response( - sys_nom, omega, squeeze=False - ) - frequency_response_offnom_list = control.frequency_response( + frd_nominal = control.frequency_response(sys_nom, omega, squeeze=False) + frd_offnominal_list = control.frequency_response( sys_offnom_list, omega, squeeze=False ) # Complex frequency response of systems - complex_response_nom = frequency_response_nom.complex.transpose(2, 0, 1) - complex_response_offnom_list = [] - for frequency_response_offnom in frequency_response_offnom_list: - complex_response_offnom_list.append( - frequency_response_offnom.complex.transpose(2, 0, 1) - ) - complex_response_offnom_list = np.array( - complex_response_offnom_list, dtype=complex + complex_nominal = frd_nominal.complex.transpose(2, 0, 1) + complex_offnominal = np.array( + [frd.complex.transpose(2, 0, 1) for frd in frd_offnominal_list] ) # Uncertainty residual computation @@ -111,8 +104,8 @@ def test_compute_uncertainty_residual_response( "inverse_multiplicative_output", ] frequency_response_residual_dict = dkpy.compute_uncertainty_residual_response( - complex_response_nom, - complex_response_offnom_list, + complex_nominal, + complex_offnominal, uncertainty_model_list, ) ndarrays_regression.check( @@ -121,7 +114,7 @@ def test_compute_uncertainty_residual_response( ) @pytest.mark.parametrize( - "complex_response_nom_freq, complex_response_offnom_freq", + "complex_response_nom, complex_response_offnom", [ ( np.array([[1, 0], [0, 1], [1, 1]]), @@ -129,20 +122,20 @@ def test_compute_uncertainty_residual_response( ), ], ) - def test_compute_uncertainty_residual_multiplicative_input_freq( - self, complex_response_nom_freq, complex_response_offnom_freq + def test_compute_residual_multiplicative_input_error( + self, complex_response_nom, complex_response_offnom ): """Test ValueError of - :func:`_compute_uncertainty_residual_multiplicative_input_freq`. + :func:`_compute_residual_multiplicative_input`. """ with pytest.raises(ValueError): - residual_freq = dkpy.uncertainty_characterization._compute_uncertainty_residual_multiplicative_input_freq( - complex_response_nom_freq, complex_response_offnom_freq + residual_freq = dkpy.uncertainty_characterization._compute_residual_multiplicative_input( + complex_response_nom, complex_response_offnom ) @pytest.mark.parametrize( - "complex_response_nom_freq, complex_response_offnom_freq", + "complex_response_nom, complex_response_offnom", [ ( np.array([[1, 0], [0, 1], [1, 1]]).T, @@ -150,19 +143,19 @@ def test_compute_uncertainty_residual_multiplicative_input_freq( ), ], ) - def test_compute_uncertainty_residual_multiplicative_output_freq( - self, complex_response_nom_freq, complex_response_offnom_freq + def test_compute_residual_multiplicative_output_error( + self, complex_response_nom, complex_response_offnom ): """Test ValueError of - :func:`_compute_uncertainty_residual_multiplicative_output_freq`. + :func:`_compute_residual_multiplicative_output`. """ with pytest.raises(ValueError): - residual_freq = dkpy.uncertainty_characterization._compute_uncertainty_residual_multiplicative_output_freq( - complex_response_nom_freq, complex_response_offnom_freq + residual_freq = dkpy.uncertainty_characterization._compute_residual_multiplicative_output( + complex_response_nom, complex_response_offnom ) @pytest.mark.parametrize( - "complex_response_nom_freq, complex_response_offnom_freq", + "complex_response_nom, complex_response_offnom", [ ( np.array([[1, 0], [0, 1], [1, 1]]), @@ -174,19 +167,21 @@ def test_compute_uncertainty_residual_multiplicative_output_freq( ), ], ) - def test_compute_uncertainty_residual_inverse_additive_freq( - self, complex_response_nom_freq, complex_response_offnom_freq + def test_compute_residual_inverse_additive_error( + self, complex_response_nom, complex_response_offnom ): """Test ValueError of - :func:`_compute_uncertainty_residual_inverse_additive_freq`. + :func:`_compute_residual_inverse_additive`. """ with pytest.raises(ValueError): - residual_freq = dkpy.uncertainty_characterization._compute_uncertainty_residual_inverse_additive_freq( - complex_response_nom_freq, complex_response_offnom_freq + residual_freq = ( + dkpy.uncertainty_characterization._compute_residual_inverse_additive( + complex_response_nom, complex_response_offnom + ) ) @pytest.mark.parametrize( - "complex_response_nom_freq, complex_response_offnom_freq", + "complex_response_nom, complex_response_offnom", [ ( np.array([[1, 0], [0, 1], [1, 1]]), @@ -194,19 +189,19 @@ def test_compute_uncertainty_residual_inverse_additive_freq( ), ], ) - def test_compute_uncertainty_residual_inverse_multiplicative_input_freq( - self, complex_response_nom_freq, complex_response_offnom_freq + def test_compute_residual_inverse_multiplicative_input_error( + self, complex_response_nom, complex_response_offnom ): """Test ValueError of - :func:`_compute_uncertainty_residual_inverse_multiplicative_input_freq`. + :func:`_compute_residual_inverse_multiplicative_input`. """ with pytest.raises(ValueError): - residual_freq = dkpy.uncertainty_characterization._compute_uncertainty_residual_inverse_multiplicative_input_freq( - complex_response_nom_freq, complex_response_offnom_freq + residual_freq = dkpy.uncertainty_characterization._compute_residual_inverse_multiplicative_input( + complex_response_nom, complex_response_offnom ) @pytest.mark.parametrize( - "complex_response_nom_freq, complex_response_offnom_freq", + "complex_response_nom, complex_response_offnom", [ ( np.array([[1, 0], [0, 1], [1, 1]]).T, @@ -214,15 +209,15 @@ def test_compute_uncertainty_residual_inverse_multiplicative_input_freq( ), ], ) - def test_compute_uncertainty_residual_inverse_multiplicative_output_freq( - self, complex_response_nom_freq, complex_response_offnom_freq + def test_compute_residual_inverse_multiplicative_output_error( + self, complex_response_nom, complex_response_offnom ): """Test ValueError of - :func:`_compute_uncertainty_residual_inverse_multiplicative_output_freq`. + :func:`_compute_residual_inverse_multiplicative_output`. """ with pytest.raises(ValueError): - residual_freq = dkpy.uncertainty_characterization._compute_uncertainty_residual_inverse_multiplicative_output_freq( - complex_response_nom_freq, complex_response_offnom_freq + residual_freq = dkpy.uncertainty_characterization._compute_residual_inverse_multiplicative_output( + complex_response_nom, complex_response_offnom ) From f320749bc2aad975b50bb89c24607e992cd7839d Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 14 May 2026 12:16:33 -0400 Subject: [PATCH 2/9] Include full uncertainty weight structure in optimal uncertainty weight respone computation. --- src/dkpy/uncertainty_characterization.py | 232 +++++++++------------ tests/test_uncertainty_characterization.py | 4 +- 2 files changed, 96 insertions(+), 140 deletions(-) diff --git a/src/dkpy/uncertainty_characterization.py b/src/dkpy/uncertainty_characterization.py index 52cc112..d9bbdcc 100644 --- a/src/dkpy/uncertainty_characterization.py +++ b/src/dkpy/uncertainty_characterization.py @@ -431,9 +431,9 @@ def _compute_residual_inverse_multiplicative_output( def compute_uncertainty_weight_response( - complex_response_residual_list: Union[np.ndarray, control.FrequencyResponseList], - weight_left_structure: str, - weight_right_structure: str, + complex_residual: Union[np.ndarray, control.FrequencyResponseList], + weight_left_structure: Literal["full", "diagonal", "scalar", "identity"], + weight_right_structure: Literal["full", "diagonal", "scalar", "identity"], solver_params: Optional[Dict[str, Any]] = None, ) -> Tuple[np.ndarray, np.ndarray]: """Compute the optimal uncertainty weight frequency response. @@ -443,16 +443,14 @@ def compute_uncertainty_weight_response( Parameters ---------- - complex_response_residual_list : Union[np.ndarray, control.FrequencyResponseList] + complex_residual : Union[np.ndarray, control.FrequencyResponseList] Frequency response of the residuals for which to compute the optimal uncertainty weights. - weight_left_structure : str - Structure of the left uncertainty weight. Valid structures include: "diagonal", - "scalar", and "identity". - weight_right_structure : str - Structure of the right uncertainty weight. Valid structures include: "diagonal", - "scalar", and "identity". - solver_param : Dict[str, Any] + weight_left_structure : Literal["full", "diagonal", "scalar", "identity"] + Structure of the left uncertainty weight. + weight_right_structure : Literal["full", "diagonal", "scalar", "identity"] + Structure of the right uncertainty weight. + solver_params : Dict[str, Any] Keyword arguments for the convex optimization solver. See [#cvxpy_solver]_ for more information. @@ -471,14 +469,14 @@ def compute_uncertainty_weight_response( >>> complex_response_nom, complex_response_offnom_list, omega = ( ... example_multimodel_uncertainty ... ) - >>> uncertainty_models = { + >>> uncertainty_models = [ ... "additive", ... "multiplicative_input", ... "multiplicative_output", ... "inverse_additive", ... "inverse_multiplicative_input", ... "inverse_multiplicative_output", - ... } + ... ] >>> complex_response_residual_dict = compute_uncertainty_residual_response( ... complex_response_nom, ... complex_response_offnom_list, @@ -497,72 +495,12 @@ def compute_uncertainty_weight_response( .. [#uncertainty_characterization] G. J. Balas, A. K. Packard, and P. J. Seiler, “Uncertain Model Set Calculation from Frequency Domain Data,” Springer eBooks, pp. 89–105, Jan. 2009, doi: https://doi.org/10.1007/978-1-4419-0895-7_6. - """ # Convert frequency response data to expected type - complex_response_residual_list = _convert_frequency_response_list_to_array( - complex_response_residual_list - ) - - # Frequency response parameters - num_frequency = complex_response_residual_list.shape[1] - - # Compute optimal uncertainty weights - complex_response_weight_left = [] - complex_response_weight_right = [] - for idx_freq in range(num_frequency): - complex_residual_freq = complex_response_residual_list[:, idx_freq, :, :] - weight_left_freq, weight_right_freq = _compute_optimal_weight_freq( - complex_residual_freq, - weight_left_structure, - weight_right_structure, - solver_params, - ) - complex_response_weight_left.append(weight_left_freq) - complex_response_weight_right.append(weight_right_freq) - - # Generate uncertainty weight complex frequency reponses - complex_response_weight_left = np.array(complex_response_weight_left) - complex_response_weight_right = np.array(complex_response_weight_right) + complex_residual = _convert_frequency_response_list_to_array(complex_residual) - return complex_response_weight_left, complex_response_weight_right - - -def _compute_optimal_weight_freq( - complex_residual_offnom_set_freq: np.ndarray, - weight_left_structure: str, - weight_right_structure: str, - solver_params: Optional[Dict[str, Any]] = None, -) -> Tuple[np.ndarray, np.ndarray]: - """Compute the optimal uncertainty weight at a given frequency. - - Parameters - ---------- - complex_residual_offnom_set_freq : np.ndarray - Frequency response matrix of the off-nominal models at a given frequency. - weight_left_structure : str - Structure of the left uncertainty weight. Valid structures include: "diagonal", - "scalar", and "identity". - weight_right_structure : str - Structure of the right uncertainty weight. Valid structures include: "diagonal", - "scalar", and "identity". - solver_param : Dict[str, Any] - Keyword arguments for the convex optimization solver. See [#cvxpy_solver]_ for - more information. - - Returns - ------- - Tuple[np.ndarray, np.ndarray] - The left and right uncertainty weight frequency response matrices at a given - frequency. - - References - ---------- - .. [#cxvpy_solver] https://www.cvxpy.org/tutorial/solvers/index.html - """ - - # Solver settings + # Parse solver parameters solver_params = ( { "solver": cvxpy.CLARABEL, @@ -576,85 +514,103 @@ def _compute_optimal_weight_freq( else solver_params ) - # System parameters - num_left = complex_residual_offnom_set_freq.shape[1] - num_right = complex_residual_offnom_set_freq.shape[2] - num_offnom = complex_residual_offnom_set_freq.shape[0] + # Frequency response parameters + nbr_frequency = complex_residual.shape[1] + nbr_left = complex_residual.shape[2] + nbr_right = complex_residual.shape[3] + nbr_offnom = complex_residual.shape[0] # Generate left weight variable - if weight_left_structure == "diagonal": - L = cvxpy.Variable((num_left, num_left), diag=True) - elif weight_left_structure == "scalar": - L_scalar = cvxpy.Variable() - L = L_scalar * scipy.sparse.eye_array(num_left) - elif weight_left_structure == "identity": - L = cvxpy.Parameter( - shape=(num_left, num_left), value=np.eye(num_left), diag=True - ) - else: - raise ValueError( - f'"{weight_left_structure}" is not a valid value for ' - '`weight_right_structure`. It must take a value of either "diagonal" ' - '"scalar", or "identity".' + weight_left_power_dispatcher = { + "full": cvxpy.Variable((nbr_left, nbr_left), hermitian=True), + "diagonal": cvxpy.Variable((nbr_left, nbr_left), diag=True), + "scalar": cvxpy.Variable() * scipy.sparse.eye_array(nbr_left), + "identity": cvxpy.Constant(value=np.eye(nbr_left)), + } + try: + weight_left_power = weight_left_power_dispatcher[weight_left_structure] + except KeyError: + raise KeyError( + '`weight_left_structure` must be "full", "diagonal", or "scalar" (got ' + f'"{weight_left_structure})".' ) # Generate right weight variable - if weight_right_structure == "diagonal": - R = cvxpy.Variable((num_right, num_right), diag=True) - elif weight_right_structure == "scalar": - R_scalar = cvxpy.Variable() - R = R_scalar * scipy.sparse.eye_array(num_right) - elif weight_right_structure == "identity": - R = cvxpy.Parameter( - shape=(num_right, num_right), value=np.eye(num_right), diag=True - ) - else: - raise ValueError( - f'"{weight_right_structure}" is not a valid value for ' - '`weight_right_structure`. It must take a value of either "diagonal" ' - '"scalar", or "identity".' + weight_right_power_dispatcher = { + "full": cvxpy.Variable((nbr_right, nbr_right), hermitian=True), + "diagonal": cvxpy.Variable((nbr_right, nbr_right), diag=True), + "scalar": cvxpy.Variable() * scipy.sparse.eye_array(nbr_right), + "identity": cvxpy.Constant(value=np.eye(nbr_right)), + } + try: + weight_right_power = weight_right_power_dispatcher[weight_right_structure] + except KeyError: + raise KeyError( + '`weight_right_structure` must be "full", "diagonal", or "scalar" (got ' + f'"{weight_right_structure})".' ) - # Generate optimal uncertainty weight constraints - constraint_freq_list = [] - for idx_offnom in range(num_offnom): - E_k = cvxpy.Parameter( - shape=(num_left, num_right), - value=complex_residual_offnom_set_freq[idx_offnom, :, :], - complex=True, - ) + # Generate residual parameters + residual_offnom = cvxpy.Parameter( + shape=(nbr_offnom, nbr_left, nbr_right), + complex=True, + ) + + # Uncertainty set constraints over all frequencies + constraint_list = [] + for idx_offnom in range(nbr_offnom): constraint_matrix_freq = cvxpy.bmat( [ - [L, E_k], - [E_k.H, R], + [weight_left_power, residual_offnom[idx_offnom, :, :]], + [residual_offnom[idx_offnom, :, :].H, weight_right_power], ] ) constraint_freq = constraint_matrix_freq >> 0 - constraint_freq_list.append(constraint_freq) - constraint_freq_list.append(L.H == L) - constraint_freq_list.append(L >> 0) - constraint_freq_list.append(R.H == R) - constraint_freq_list.append(R >> 0) + constraint_list.append(constraint_freq) + + # Positive semidefiniteness constraints + constraint_list.append(weight_left_power >> 0) + constraint_list.append(weight_right_power >> 0) # Semidefinite program - objective = cvxpy.Minimize(cvxpy.trace(L) + cvxpy.trace(R)) - problem = cvxpy.Problem(objective, constraint_freq_list) - problem.solve(**solver_params) + objective = cvxpy.Minimize( + nbr_right * cvxpy.trace(weight_left_power) + + nbr_left * cvxpy.trace(weight_right_power) + ) + problem = cvxpy.Problem(objective, constraint_list) - # Extract left weight - if weight_left_structure == "identity": - L_value = np.array(L.value) - else: - L_value = np.array(L.value.toarray()) - complex_response_weight_left_freq = np.sqrt(L_value) - # Extract right weight - if weight_right_structure == "identity": - R_value = np.array(R.value) - else: - R_value = np.array(R.value.toarray()) - complex_response_weight_right_freq = np.sqrt(R_value) + # Compute optimal uncertainty weights + complex_weight_left = [] + complex_weight_right = [] + for idx_freq in range(nbr_frequency): + # Residual of off-nominal models at given frequency + residual_offnom.value = complex_residual[:, idx_freq, :, :] + + # Solve optimal weight SDP + problem.solve(canon_backend=cvxpy.SCIPY_CANON_BACKEND, **solver_params) + + # Extract left weight + if weight_left_structure == "identity" or weight_left_structure == "full": + weight_left_power_opt = np.array(weight_left_power.value) + else: + weight_left_power_opt = np.array(weight_left_power.value.toarray()) + weight_left_opt = scipy.linalg.sqrtm(weight_left_power_opt) + + # Extract right weight + if weight_right_structure == "identity" or weight_right_structure == "full": + weight_right_power_opt = np.array(weight_right_power.value) + else: + weight_right_power_opt = np.array(weight_right_power.value.toarray()) + weight_right_opt = scipy.linalg.sqrtm(weight_right_power_opt) + + complex_weight_left.append(weight_left_opt) + complex_weight_right.append(weight_right_opt) + + # Generate uncertainty weight complex frequency reponses + complex_weight_left = np.array(complex_weight_left) + complex_weight_right = np.array(complex_weight_right) - return complex_response_weight_left_freq, complex_response_weight_right_freq + return complex_weight_left, complex_weight_right def fit_uncertainty_weight( diff --git a/tests/test_uncertainty_characterization.py b/tests/test_uncertainty_characterization.py index 915b32b..5ba0ce1 100644 --- a/tests/test_uncertainty_characterization.py +++ b/tests/test_uncertainty_characterization.py @@ -636,7 +636,7 @@ def test_compute_uncertainty_weight_response( # Regression testing ndarrays_regression.check( complex_response_weight_dict, - default_tolerance=dict(atol=1e-5, rtol=0), + default_tolerance=dict(atol=1e-3, rtol=0), ) @@ -893,7 +893,7 @@ def test_fit_uncertainty_weight( # Regression testing ndarrays_regression.check( complex_response_weight_fit_dict, - default_tolerance=dict(atol=1e-5, rtol=0), + default_tolerance=dict(atol=1e-3, rtol=0), ) From f997415f9936af4ccd9305dac7c6c6b661d6fdad Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 14 May 2026 12:31:09 -0400 Subject: [PATCH 3/9] Restructure uncertainty weight fit code. --- src/dkpy/__init__.py | 1 + src/dkpy/lti_system_fit.py | 739 ++++++++++++++++++ src/dkpy/uncertainty_characterization.py | 323 ++++++-- tests/test_uncertainty_characterization.py | 62 +- ...fnom_list0_omega0_diagonal_diagonal_4_.npz | Bin 3523 -> 3511 bytes ...fnom_list1_omega1_diagonal_diagonal_4_.npz | Bin 3559 -> 3562 bytes ...fnom_list2_omega2_diagonal_diagonal_4_.npz | Bin 6795 -> 6790 bytes ...3_omega3_diagonal_diagonal_fit_order3_.npz | Bin 6795 -> 6790 bytes ...4_omega4_diagonal_diagonal_fit_order4_.npz | Bin 6795 -> 0 bytes 9 files changed, 1009 insertions(+), 116 deletions(-) create mode 100644 src/dkpy/lti_system_fit.py delete mode 100644 tests/test_uncertainty_characterization/test_fit_uncertainty_weight_sys_nom4_sys_offnom_list4_omega4_diagonal_diagonal_fit_order4_.npz diff --git a/src/dkpy/__init__.py b/src/dkpy/__init__.py index 4a48a14..6a944bb 100644 --- a/src/dkpy/__init__.py +++ b/src/dkpy/__init__.py @@ -3,6 +3,7 @@ from .controller_synthesis import * from .dk_iteration import * from .d_scale_fit import * +from .lti_system_fit import * from .structured_singular_value import * from .uncertainty_structure import * from .uncertainty_characterization import * diff --git a/src/dkpy/lti_system_fit.py b/src/dkpy/lti_system_fit.py new file mode 100644 index 0000000..5b27af1 --- /dev/null +++ b/src/dkpy/lti_system_fit.py @@ -0,0 +1,739 @@ +"""LTI system fit utilities.""" + +from typing import Any, Dict, Union, Optional, Tuple, List + +import numpy as np +import control +import cvxpy + +__all__ = [ + "fit_magnitude_siso_ct", + "fit_power_siso_dt", +] + + +def fit_magnitude_siso_ct( + magnitude_fit: np.ndarray, + omega: np.ndarray, + order: int, + magnitude_upper_bound: Optional[np.ndarray] = None, + magnitude_lower_bound: Optional[np.ndarray] = None, + weight: Optional[np.ndarray] = None, + solver_params: Optional[Dict[str, Any]] = None, + tol_bisection: float = 1e-3, + max_iter_bisection: int = 500, + max_iter_bisection_init: int = 15, + nbr_power_constraint: int = 500, +) -> control.StateSpace: + """Fit a stable and minimum-phase biproper SISO system to magnitude. + + Parameters + ---------- + omega : np.ndarray + Angular frequencies (rad/s). + magnitude_fit : np.ndarray + Magnitude response to fit the LTI system. + order : int + Order of the LTI system. + magnitude_upper_bound : Optional[np.ndarray] + Magnitude response for the upper bound constraint on the fitted LTI system + magnitude response. + magnitude_lower_bound : Optional[np.ndarray] + Magnitude response for the lower bound constraint on the fitted LTI system + magnitude response. + weight : np.ndarray + Frequency-dependent weight to encode bandwidths over which to prioritize the + accuracy of the LTI system fit. + solver_param : Dict[str, Any] + Solver parameters for the optimization problem. These are keyword arguments for + `cvxpy.Problem.solve()` [#cvxpy_solver]_. + tol_bisection : float + Numerical tolerance for the bisection algorithm. + max_iter_bisection : int + Maximum allowable number of iterations in the bisection algorithm. + max_iter_bisection_init : int + Maximum number of iterations for the bisection algorithm initialization. + nbr_power_constraint : Optional[np.ndarray] + Number of frequencies to enforce non-negativity of power spectrum. + + Returns + ------- + control.StateSpace + Fitted state-space system. + + Notes + ----- + The algorithm fits the LTI system to the magnitude data as follows: + 1) Pre-warp the continuous-time frequencies to discrete-time frequencies using a + bilinear transformation. + 2) Fit a discrete-time power spectrum model to the square of the magnitude data. + 3) Extract the stable and minimum-phase spectral factor from the power spectrum + model. + 4) Convert the discrete-time spectral factor model to a continuous-time model + using the bilinear transformation used in step 1). + + References + ---------- + .. [#cxvpy_solver] https://www.cvxpy.org/tutorial/solvers/index.html + """ + + # Compute power spectrum + power_fit = np.abs(magnitude_fit) ** 2 + power_upper_bound = ( + np.abs(magnitude_upper_bound) ** 2 + if magnitude_upper_bound is not None + else None + ) + power_lower_bound = ( + np.abs(magnitude_lower_bound) ** 2 + if magnitude_lower_bound is not None + else None + ) + + # Pre-warp frequency (continuous- to discrete-time) with bilinear transformation + alpha = np.sqrt(omega[0] * omega[-1]) + theta = 2 * np.atan(omega / alpha) + + # Parse frequency-dependent weight + if weight is None: + weight = np.ones_like(omega) + + # Discrete-time power spectrum fit coefficients + num_power_coef, den_power_coef = fit_power_siso_dt( + power_fit, + theta, + order, + weight, + power_upper_bound, + power_lower_bound, + solver_params, + tol_bisection, + max_iter_bisection, + max_iter_bisection_init, + nbr_power_constraint, + ) + + # Discrete-time spectral factor model + num_factor_coef, den_factor_coef = _compute_spectral_factor_siso_dt( + num_power_coef, den_power_coef + ) + tf_factor_dt = control.TransferFunction(num_factor_coef, den_factor_coef, True) + + # Continous-time state-space + ss_factor_ct = _convert_discrete_to_continuous_bilinear(tf_factor_dt, alpha) + + return ss_factor_ct + + +def fit_power_siso_dt( + power_fit: np.ndarray, + theta: np.ndarray, + order: int, + weight: Optional[np.ndarray] = None, + power_upper_bound: Optional[np.ndarray] = None, + power_lower_bound: Optional[np.ndarray] = None, + solver_params: Optional[Dict[str, Any]] = None, + tol_bisection: float = 1e-5, + max_iter_bisection: int = 100, + max_iter_bisection_init: int = 15, + nbr_power_constraint: int = 500, +) -> Tuple[np.ndarray, np.ndarray]: + """Fit a biproper discrete-time SISO power spectrum transfer function. + + Parameters + ---------- + power_fit : np.ndarray + Power spectrum data to fit model. + theta : np.ndarray + Discrete-time angular frequencies (-). The frequencies range from [0, pi]. + order : int + Order of the power spectrum model. + weight : Optional[np.ndarray] + Frequency-dependent weight for fit accuracy. + power_upper_bound : np.ndarray + Power spectrum data to upper bound the model frequency response. + power_lower_bound : np.ndarray + Power spectrum data to lower bound the model frequency response. + solver_params: Optional[Dict[str, Any]] + Solver parameters for the optimization problem. These are keyword arguments for + `cvxpy.Problem.solve()` [#cvxpy_solver]_. + tol_bisection : float + Numerical tolerance for the bisection algorithm. + max_iter_bisection : int + Maximum number of iterations for the bisection algorithm. + max_iter_bisection_init : int + Maximum number of iterations for the bisection algorithm initialization. + nbr_power_constraint : Optional[np.ndarray] + Number of frequencies to enforce non-negativity of power spectrum. + + Returns + ------- + Tuple[np.ndarray, np.ndarray] + Numerator and denominator polynomial coefficients of the power spectrum. + + Notes + ----- + The algorithm fits a power spectrum of the form + b_{m} z^{-m} + ... + b_{0} + ... + b_{m} z^{m} + R(z) = ---------------------------------------------- + a_{m} z^{-m} + ... + a_{0} + ... + b_{m} z^{m} + + The algorithm uses a log-Chebyshev approximation method, which results in an + optimization problem that is solved by bisecting a bound on the fit error and + solving linear feasibility problems [#log_chebyshev]_. + + References + ---------- + .. [#cxvpy_solver] https://www.cvxpy.org/tutorial/solvers/index.html + + .. [#log_chebyshev] Shao-Po Wu, S. Boyd and L. Vandenberghe, "FIR filter design via + semidefinite programming and spectral factorization," Proceedings of 35th IEEE + Conference on Decision and Control, Kobe, Japan, 1996, pp. 271-276 vol.1, + doi: 10.1109/CDC.1996.574313. + """ + + # Parse arguments + solver_params = _parse_solver_param(solver_params) + weight = np.ones_like(theta) if weight is None else weight + + # Optimization variables and parameters + num_coef = cvxpy.Variable(shape=order + 1, name="num_coef") + den_coef = cvxpy.Variable(shape=order + 1, name="den_coef") + error_bound = cvxpy.Parameter(shape=(), name="error_bound") + + # Constraints + constraint_list = _generate_constraints_power_siso_dt( + num_coef, + den_coef, + error_bound, + power_fit, + power_upper_bound, + power_lower_bound, + theta, + order, + weight, + nbr_power_constraint, + ) + + # Optimization problem + objective = cvxpy.Minimize(1) + problem = cvxpy.Problem(objective, constraint_list) + + # Bisection solution + error_bound_max = _initialize_optimization_bisection_upper_bound( + problem, error_bound, solver_params, max_iter_bisection_init + ) + error_bound_min = 0 + _solve_optimization_bisection( + problem, + error_bound, + error_bound_max, + error_bound_min, + tol_bisection, + max_iter_bisection, + solver_params, + ) + + # Extract power spectrum coefficients + num_coef_fit = np.array(num_coef.value) + den_coef_fit = np.array(den_coef.value) + + # Construct power spectrum polynomials from unique coefficients + num_fit = np.concatenate([num_coef_fit[::-1], num_coef_fit[1:]]) + den_fit = np.concatenate([den_coef_fit[::-1], den_coef_fit[1:]]) + + return num_fit, den_fit + + +def _generate_constraints_power_siso_dt( + num_coef: cvxpy.Variable, + den_coef: cvxpy.Variable, + error_bound: cvxpy.Parameter, + power_fit: np.ndarray, + power_upper_bound: Optional[np.ndarray], + power_lower_bound: Optional[np.ndarray], + theta: np.ndarray, + order: int, + weight: np.ndarray, + nbr_power_constraints: int, +) -> List[cvxpy.Constraint]: + """Generate the constraints for the discrete-time SISO power spectrums. + + Parameters + ---------- + num_coef : cvxpy.Variable + Power spectrum numerator coefficient variable. + den_coef : cvxpy.Variable + Power spectrum denominator coefficient variable. + error_bound : cvxpy.Parameter + Power spectrum fit error bound that is minimized by the bisection algorithm. + power_fit: np.ndarray + Power spectrum data to fit model. + power_upper_bound : np.ndarray + Power spectrum data to upper bound the model frequency response. + power_lower_bound : np.ndarray + Power spectrum data to lower bound the model frequency response. + theta : np.ndarray + Discrete-time angular frequencies (-). The frequencies range from [0, pi]. + order : int + Order of the power spectrum model. + weight : np.ndarray + Frequency-dependent weight for fit accuracy. + nbr_power_constraint : int + Discrete-time frequencies to enforce non-negativity of power spectrum. + + Returns + ------- + List[cvxpy.Constraint] + List of constraints. + """ + # Fit error and magnitude bound constraints + constraint_bound_list = _construct_bound_constraints_siso_dt( + num_coef, + den_coef, + error_bound, + power_fit, + power_upper_bound, + power_lower_bound, + theta, + order, + weight, + ) + + # Spectral factorization constraint + constraint_nonneg_list = _construct_spectral_factorization_constraint_siso_dt( + num_coef, + den_coef, + order, + nbr_power_constraints, + ) + + # Normalization of the transfer function coefficients + constraint_normalization_list = [den_coef[-1] == 1] + + constraint_list = [] + constraint_list += ( + constraint_bound_list + constraint_nonneg_list + constraint_normalization_list + ) + + return constraint_list + + +def _construct_bound_constraints_siso_dt( + num_coef: cvxpy.Variable, + den_coef: cvxpy.Variable, + error_bound: cvxpy.Parameter, + power_fit: np.ndarray, + power_upper_bound: Optional[np.ndarray], + power_lower_bound: Optional[np.ndarray], + theta: np.ndarray, + order: int, + weight: np.ndarray, +) -> List[cvxpy.Constraint]: + """Construct the fit error and power spectrum bound linear constraints. + + The fit error and power spectrum bound linear constraints are used in the fit of + discrete-time SISO power spectrums. + + Parameters + ---------- + num_coef : cvxpy.Variable + Power spectrum numerator coefficient variable. + den_coef : cvxpy.Variable + Power spectrum denominator coefficient variable. + error_bound : cvxpy.Parameter + Fit error parameter that is minimized by the bisection algorithm. + power_fit: np.ndarray + Power spectrum data to fit model. + power_upper_bound : np.ndarray + Power spectrum data to upper bound the model frequency response. + power_lower_bound : np.ndarray + Power spectrum data to lower bound the model frequency response. + theta : np.ndarray + Discrete-time angular frequencies (-). The frequencies range from [0, pi]. + order : int + Order of the power spectrum model. + weight : np.ndarray + Frequency-dependent weight for fit accuracy. + + Returns + ------- + List[cvxpy.Constraint] + List of fit error and magnitude bound linear constraints. + """ + + # Initialize constraint lists + constraint_fit_upper_list = [] + constraint_fit_lower_list = [] + constraint_bound_upper_list = [] + constraint_bound_lower_list = [] + + # Check if the upper/lower bound magnitude is the same as the fit magnitude. If it + # is, the upper/lower fit error constraint may be removed as it is made redundant by + # the bound constraint. + is_power_fit_upper_bound = ( + np.allclose(power_fit, power_upper_bound) + if power_upper_bound is not None + else False + ) + is_power_fit_lower_bound = ( + np.allclose(power_fit, power_lower_bound) + if power_lower_bound is not None + else False + ) + + # Construct constraints at each frequency + for idx in range(theta.size): + # Constraint data + theta_idx = theta[idx] + power_idx = power_fit[idx] + weight_idx = weight[idx] + + # Constraint data matrices + cosine_row = np.array( + [2 * np.cos(idx_coef * theta_idx) for idx_coef in range(order + 1)] + ) + cosine_row[0] = 1 + + # Fit error upper bound constraint + if not is_power_fit_upper_bound: + constraint_fit_upper = ( + -(1 + error_bound / weight_idx) + * power_idx + * cvxpy.vdot(cosine_row, den_coef) + + cvxpy.vdot(cosine_row, num_coef) + <= 0 + ) + constraint_fit_upper_list.append(constraint_fit_upper) + + # Fit error lower bound constraint + if not is_power_fit_lower_bound: + constraint_fit_lower = ( + power_idx * cvxpy.vdot(cosine_row, den_coef) + - (1 + error_bound / weight_idx) * cvxpy.vdot(cosine_row, num_coef) + <= 0 + ) + constraint_fit_lower_list.append(constraint_fit_lower) + + # Upper bound constraint + if power_upper_bound is not None: + power_upper_idx = power_upper_bound[idx] + constraint_bound_upper = ( + -power_upper_idx * cvxpy.vdot(cosine_row, den_coef) + - cvxpy.vdot(cosine_row, num_coef) + <= 0 + ) + constraint_bound_upper_list.append(constraint_bound_upper) + + # Lower bound constraint + if power_lower_bound is not None: + power_lower_idx = power_lower_bound[idx] + constraint_bound_lower = ( + power_lower_idx * cvxpy.vdot(cosine_row, den_coef) + - cvxpy.vdot(cosine_row, num_coef) + <= 0 + ) + constraint_bound_lower_list.append(constraint_bound_lower) + + return ( + constraint_fit_upper_list + + constraint_fit_lower_list + + constraint_bound_upper_list + + constraint_bound_lower_list + ) + + +def _construct_spectral_factorization_constraint_siso_dt( + num_coef: cvxpy.Variable, + den_coef: cvxpy.Variable, + order: int, + nbr_theta: np.ndarray, +) -> List[cvxpy.Constraint]: + """Construct power spectrum spectral factorization constraints. + + The power spectrum spectral factorization constraints are used in the fit of + discrete-time SISO power spectrums. + + Parameters + ---------- + num_coef : cvxpy.Variable + Power spectrum numerator coefficient variable. + den_coef : cvxpy.Variable + Power spectrum denominator coefficient variable. + order : int + Order of the power spectrum model. + nbr_theta : int + Number of frequencies to enforce constraint. + + Returns + ------- + List[cvxpy.Constraint] + List of spectral factorization (power spectrum non-negativity) linear + constraints. + """ + + # Frequencies to enforce power spectrum non-negativity + theta = np.linspace(0, np.pi, nbr_theta) + + constraint_spectral_factorization_list = [] + for theta_idx in theta: + # Constraint data matrices + cosine_row = np.array( + [2 * np.cos(idx_coef * theta_idx) for idx_coef in range(order + 1)] + ) + cosine_row[0] = 1 + + # Spectral factorization constraints + constraint_num = cvxpy.vdot(cosine_row, num_coef) >= 0 + constraint_den = cvxpy.vdot(cosine_row, den_coef) >= 0 + constraint_spectral_factorization_list += [constraint_num, constraint_den] + + return constraint_spectral_factorization_list + + +def _initialize_optimization_bisection_upper_bound( + problem: cvxpy.Problem, + objective: cvxpy.Parameter, + solver_params: Dict[str, Any], + max_iter_bisection_init: int = 15, +) -> float: + """Initialize the upper bound of a bisection optimization problem. + + Parameters + ---------- + problem : cvxpy.Problem + Optimization problem. + objective : cvxpy.Parameter + Objective function that the bisection algorithm minimizes. + solver_params : Dict[str, Any] + Solver parameters for the optimization problem. These are keyword arguments for + `cvxpy.Problem.solve()` [#cvxpy_solver]_. + max_iter_bisection_init : int + Maximum allowable number of iterations in the bisection initialization. + + Returns + ------- + float + Feasible upper bound for the bisection optimization algorithm. + + References + ---------- + .. [#cxvpy_solver] https://www.cvxpy.org/tutorial/solvers/index.html + """ + objective.value = 0.5 + + for _ in range(max_iter_bisection_init): + objective.value *= 2 + try: + problem.solve(**solver_params) + feasibility_status = problem.status + except cvxpy.SolverError: + break + + if feasibility_status == "optimal": + return objective.value + + raise ValueError( + "Unable to determine a upper bound on the objective to initialize " + "the bisection algorithm." + ) + + +def _solve_optimization_bisection( + problem: cvxpy.Problem, + objective: cvxpy.Parameter, + objective_max: float, + objective_min: float, + tol_bisection: float, + max_iter_bisection: int, + solver_params: Dict[str, Any], +) -> None: + """Solve an optimization problem using a bisection algorithm. + + Parameters + ---------- + problem : cvxpy.Problem + Optimization problem. + objective : cvxpy.Parameter + Objective function that the bisection algorithm minimizes. + objective_max : float + Initial upper bound on objective. + objective_min : float + Initial lower bound on objective. + tol_bisection : float + Numerical tolerance for the bisection algorithm. + max_iter_bisection : int + Maximum allowable number of iterations in the bisection algorithm. + solver_params : Dict[str, Any] + Solver parameters for the optimization problem. These are keyword arguments for + `cvxpy.Problem.solve()` [#cvxpy_solver]_. + + References + ---------- + .. [#cxvpy_solver] https://www.cvxpy.org/tutorial/solvers/index.html + """ + + # Initialize bisection algorithm + iter_bisection = 0 + feasibility_status = "infeasible" + + # Bisection minimization of objective + while ( + np.abs(objective_max - objective_min) >= tol_bisection + or feasibility_status != "optimal" + ): + # Increment bisection iteration count + iter_bisection += 1 + + # Stop bisection at maximum iterations + if iter_bisection >= max_iter_bisection: + break + + # Bisect objective function + objective.value = 0.5 * (objective_max + objective_min) + try: + problem.solve(**solver_params) + feasibility_status = problem.status + except cvxpy.SolverError: + objective_min = objective.value + + # Update upper and lower bisection bounds + if feasibility_status == "optimal": + objective_max = objective.value + else: + objective_min = objective.value + + +def _compute_spectral_factor_siso_dt( + num_power: np.ndarray, den_power: np.ndarray +) -> Tuple[np.ndarray, np.ndarray]: + """Compute the spectral factorization of a discrete-time SISO transfer function. + + Parameters + ---------- + num_power : np.ndarray + Power spectrum numerator polynomial. + den_power : np.ndarray + Power spectrum denominator polynomial. + + Returns + ------- + Tuple[np.ndarray, np.ndarray] + Spectral factor numerator and denominator polynomial. + + Notes + ----- + The algorithm takes a discrete-time power spectrum of the form + b_{m} z^{-m} + ... + b_{0} + ... + b_{m} z^{m} + R(z) = ---------------------------------------------- + a_{m} z^{-m} + ... + a_{0} + ... + b_{m} z^{m} + + and computes the spectral factor of the form + d_{m} z^{m} + ... + d_{0} + F(z) = ------------------------- + c_{m} z^{m} + ... + c_{0} + + The algorithm computes the spectral factor by computing the poles and zeros of the + power spectrum. Then, the DC gain, stable poles, and minimum-phase zeros are + extracted and used to construct the stable and minimum-phase spectral factor. + """ + + # Power spectrum poles and zeros + zeros_power = np.roots(num_power) + poles_power = np.roots(den_power) + + # Stable and minimum-phase power spectrum poles and zeros + zeros_power_stable = zeros_power[np.abs(zeros_power) < 1] + poles_power_stable = poles_power[np.abs(poles_power) < 1] + + # Spectral factorization + spectral_const_num = np.sqrt(num_power[0] / np.prod(-zeros_power_stable)) + spectral_const_den = np.sqrt(den_power[0] / np.prod(-poles_power_stable)) + num_factor = np.real( + spectral_const_num + * np.polynomial.polynomial.polyfromroots(zeros_power_stable)[::-1] + ) + den_factor = np.real( + spectral_const_den + * np.polynomial.polynomial.polyfromroots(poles_power_stable)[::-1] + ) + + return num_factor, den_factor + + +def _convert_discrete_to_continuous_bilinear( + sys_dt: Union[control.TransferFunction, control.StateSpace], + alpha: float, +) -> control.StateSpace: + """Convert a discrete-time system to continuous-time with a bilinear transformation. + + Parameters + ---------- + sys_dt : Union[control.TransferFunction, control.StateSpace] + Discrete-time system. + alpha : float + Bilinear transformation constant. + + Returns + ------- + control.StateSpace + Continuous-time system. + """ + # Convert transfer function to state-space system + sys_dt = control.StateSpace(control.ss(sys_dt)) + + # Discrete-time state-space matrices + Ad = sys_dt.A + Bd = sys_dt.B + Cd = sys_dt.C + Dd = sys_dt.D + + # Additional matrices + In = np.eye(Ad.shape[0]) + In_Ad_inv = np.linalg.solve(In + Ad, In) + + # Continous-time matrices + Ac = alpha * (Ad - In) @ In_Ad_inv + Bc = alpha * (In - (Ad - In) @ In_Ad_inv) @ Bd + Cc = Cd @ In_Ad_inv + Dc = Dd - Cd @ In_Ad_inv @ Bd + + # Continuous-time system + sys_dt = control.StateSpace(Ac, Bc, Cc, Dc) + + return sys_dt + + +def _parse_solver_param(solver_params: Optional[Dict[str, Any]]) -> Dict[str, Any]: + """Parse solver parameters and ensure warm-start is enabled. + + Parameters + ---------- + solver_params: Optional[Dict[str, Any]] + Solver parameters for the optimization problem. These are keyword arguments for + `cvxpy.Problem.solve()` [#cvxpy_solver]_. + + Returns + ------- + Dict[str, Any] + Parsed solver parameters for the optimization problem. These are keyword + arguments for `cvxpy.Problem.solve()` [#cvxpy_solver]_. + + References + ---------- + .. [#cxvpy_solver] https://www.cvxpy.org/tutorial/solvers/index.html + """ + # Default solver parameters + solver_param_default = { + "solver": "CLARABEL", + "warm_start": True, + "verbose": False, + } + + if solver_params is None: + return solver_param_default + else: + # Ensure that `warm_start` is set to True as the bisection algorithm is greatly + # sped up if this holds. + solver_params["warm_start"] = True + return solver_params diff --git a/src/dkpy/uncertainty_characterization.py b/src/dkpy/uncertainty_characterization.py index d9bbdcc..9447b22 100644 --- a/src/dkpy/uncertainty_characterization.py +++ b/src/dkpy/uncertainty_characterization.py @@ -25,7 +25,7 @@ from matplotlib.axes import Axes from matplotlib.legend import Legend -from . import utilities +from . import lti_system_fit UncertaintyModelId = Literal[ "additive", @@ -614,48 +614,48 @@ def compute_uncertainty_weight_response( def fit_uncertainty_weight( - complex_response_uncertainty_weight: Union[ - np.ndarray, control.FrequencyResponseData - ], + complex_uncertainty_weight: Union[np.ndarray, control.FrequencyResponseData], omega: np.ndarray, order: Union[int, List[int], np.ndarray], + uncertainty_weight_type: Literal["left", "right"], + uncertainty_weight_structure: Literal["scalar", "diagonal", "full"], weight: Optional[np.ndarray] = None, - linear_solver_params: Optional[Dict[str, Any]] = None, + solver_params: Optional[Dict[str, Any]] = None, tol_bisection: float = 1e-3, max_iter_bisection: int = 500, - num_spec_constr: int = 500, + max_iter_bisection_init: int = 15, + nbr_power_constraint: int = 500, ) -> control.StateSpace: - """Fit an overbounding stable and minimum-phase state-space uncertainty weight to - frequency response data. + """Fit an overbounding stable and minimum-phase uncertainty weight. Parameters ---------- - complex_response_uncertainty_weight : Union[np.ndarray, control.FrequencyResponseData] - Uncertainty weight frequency response used for the overbounding fit. + complex_uncertainty_weight : Union[np.ndarray, control.FrequencyResponseData] + Frequency response of uncertainty weight. order : Union[int, List[int], np.ndarray] - Order of the LTI system fit. If `order` is an `int`, the order will be - used for all elements of the weight. If `order` is a `List` or `np.ndarray`, - the order can be specified for each element of the weight. - weight : Optional[np.ndarray] = None - Frequency-dependent weight used to improve the fit over certain bandwidths. The - weight is a 2D array with the first dimension representing the number of - elements in the weight and the second dimension representing the number of - frequency points. - linear_solver_params : Dict[str, Any] - Keyword arguments for the linear feasibility problem solver. See - [#cvxpy_solver]_ for more information. + Order of the uncertainty weight model. + uncertainty_weight_type: Literal["left", "right"], + Identifier for the left or right uncertainty weight. + uncertainty_weight_structure : Literal["scalar", "diagonal", "full"] + Structure constraint for the uncertainty weight. + weight : Optional[np.ndrray] = None + Frequency-dependent weight for fit accuracy. + solver_params: Optional[Dict[str, Any]] + Solver parameters for the optimization problem. These are keyword arguments for + `cvxpy.Problem.solve()` [#cvxpy_solver]_. tol_bisection : float Numerical tolerance for the bisection algorithm. max_iter_bisection : int - Maximum allowable number of iterations in the bisection algorithm. - num_spec_constr : int - Number of constraints used to enforce the spectral factorizability of the - fitted autocorrelation. + Maximum number of iterations for the bisection algorithm. + max_iter_bisection_init : int + Maximum number of iterations for the bisection algorithm initialization. + nbr_power_constraint : Optional[np.ndarray] + Number of frequencies to enforce non-negativity of power spectrum. Returns ------- control.StateSpace - Fitted overbounding uncertainty weight state-space systems. + Overbounding stable and minimum-phase uncertainty weight. Examples -------- @@ -686,10 +686,10 @@ def fit_uncertainty_weight( ... ) ... ) >>> weight_left = dkpy.fit_uncertainty_weight( - ... complex_response_weight_left, omega, [4, 5] + ... complex_response_weight_left, omega, [4, 5], "left", "diagonal" ... ) >>> weight_right = dkpy.fit_uncertainty_weight( - ... complex_response_weight_right, omega, [3, 5] + ... complex_response_weight_right, omega, [3, 5], "right", "diagonal" ... ) References @@ -698,12 +698,12 @@ def fit_uncertainty_weight( """ # Convert frequency response data to expected type - complex_response_uncertainty_weight = _convert_frequency_response_data_to_array( - complex_response_uncertainty_weight + complex_uncertainty_weight = _convert_frequency_response_data_to_array( + complex_uncertainty_weight ) # Solver settings - linear_solver_params = ( + solver_params = ( { "solver": cvxpy.CLARABEL, "tol_gap_abs": 1e-9, @@ -712,56 +712,249 @@ def fit_uncertainty_weight( "tol_infeas_abs": 1e-9, "tol_infeas_rel": 1e-9, } - if linear_solver_params is None - else linear_solver_params + if solver_params is None + else solver_params ) - # Parse arguments - num_elements = complex_response_uncertainty_weight.shape[1] - order_list = ( - order * np.ones(num_elements, dtype=int) - if isinstance(order, int) - else np.array(order, dtype=int) - ) + # Fit method dispatcher + fit_method_dispatcher = { + "scalar": _fit_uncertainty_weight_scalar, + "diagonal": _fit_uncertainty_weight_diagonal, + "full": _fit_uncertainty_weight_full, + } - if weight is None: - # Take the default frequency-dependent weight as the normalized magnitude the - # uncertainty weight magnitude in order to place greater importance on tightly - # overbounding at the largest uncertainties - weight = np.diagonal( - np.abs(complex_response_uncertainty_weight), axis1=1, axis2=2 + # Select uncertainty weight fit method based on weight structure + try: + fit_uncertainty_weight_method = fit_method_dispatcher[ + uncertainty_weight_structure + ] + except KeyError: + raise KeyError( + "Exptected `weight_structure` to be `scalar`, `diagonal` or `full` (got " + f"`{uncertainty_weight_structure}`)." ) - weight = weight / np.max(weight, axis=0) - uncertainty_weight_list = [] - for idx_element in range(num_elements): - # Extract the parameters relevant to each SISO uncertainty weight element - magnitude_response_weight_element = np.abs( - complex_response_uncertainty_weight[:, idx_element, idx_element] - ) - order_element = order_list[idx_element] - weight_element = weight[:, idx_element] + # Fit uncertainty weight + uncertainty_weight = fit_uncertainty_weight_method( + complex_uncertainty_weight, + omega, + order, + uncertainty_weight_type, + weight, + solver_params, + tol_bisection, + max_iter_bisection, + max_iter_bisection_init, + nbr_power_constraint, + ) + + return uncertainty_weight + + +def _fit_uncertainty_weight_scalar( + complex_uncertainty_weight: np.ndarray, + omega: np.ndarray, + order: int, + uncertainty_weight_type: Literal["left", "right"], + weight: Optional[np.ndarray] = None, + solver_params: Optional[Dict[str, Any]] = None, + tol_bisection: float = 1e-3, + max_iter_bisection: int = 500, + max_iter_bisection_init: int = 15, + nbr_power_constraint: int = 500, +) -> control.StateSpace: + """Fit an overbounding stable and minimum-phase scalar uncertainty weight. + + Parameters + ---------- + complex_uncertainty_weight : Union[np.ndarray, control.FrequencyResponseData] + Frequency response of uncertainty weight. + order : Union[int, List[int], np.ndarray] + Order of the uncertainty weight model. + uncertainty_weight_type: Literal["left", "right"], + Identifier for the left or right uncertainty weight. + weight : Optional[np.ndrray] = None + Frequency-dependent weight for fit accuracy. + solver_params: Optional[Dict[str, Any]] + Solver parameters for the optimization problem. These are keyword arguments for + `cvxpy.Problem.solve()` [#cvxpy_solver]_. + tol_bisection : float + Numerical tolerance for the bisection algorithm. + max_iter_bisection : int + Maximum number of iterations for the bisection algorithm. + max_iter_bisection_init : int + Maximum number of iterations for the bisection algorithm initialization. + nbr_power_constraint : Optional[np.ndarray] + Number of frequencies to enforce non-negativity of power spectrum. + + Returns + ------- + control.StateSpace + Overbounding stable and minimum-phase uncertainty weight. + + References + ---------- + .. [#cxvpy_solver] https://www.cvxpy.org/tutorial/solvers/index.html + """ + + # Auxiliary parameters + nbr_signals = complex_uncertainty_weight.shape[1] + + # Compute the magnitude of the scalar weight. Given that a scalar weight is assumed, + # the weight contains the same values on the diagonals and zeros elsewhere. + # Therefore, we take the first diagonal as they are identical along the diagonal. + magnitude_uncertainty_weight = np.abs(complex_uncertainty_weight[:, 0, 0]) + + # Fit the scalar uncertainty weight + uncertainty_weight_scalar = lti_system_fit.fit_magnitude_siso_ct( + magnitude_fit=magnitude_uncertainty_weight, + omega=omega, + order=order, + magnitude_upper_bound=None, + magnitude_lower_bound=magnitude_uncertainty_weight, + weight=weight, + solver_params=solver_params, + tol_bisection=tol_bisection, + max_iter_bisection=max_iter_bisection, + max_iter_bisection_init=max_iter_bisection_init, + nbr_power_constraint=nbr_power_constraint, + ) + + # Duplicate the scalar uncertainty weight along the diagonal + uncertainty_weight = control.append(*([uncertainty_weight_scalar] * nbr_signals)) + + return uncertainty_weight + + +def _fit_uncertainty_weight_diagonal( + complex_uncertainty_weight: np.ndarray, + omega: np.ndarray, + order: Union[int, List[int]], + uncertainty_weight_type: Literal["left", "right"], + weight: Optional[np.ndarray] = None, + solver_params: Optional[Dict[str, Any]] = None, + tol_bisection: float = 1e-3, + max_iter_bisection: int = 500, + max_iter_bisection_init: int = 15, + nbr_power_constraint: int = 500, +): + """Fit an overbounding stable and minimum-phase diagonal uncertainty weight. + + Parameters + ---------- + complex_uncertainty_weight : Union[np.ndarray, control.FrequencyResponseData] + Frequency response of uncertainty weight. + order : Union[int, List[int], np.ndarray] + Order of the uncertainty weight model. + uncertainty_weight_type: Literal["left", "right"], + Identifier for the left or right uncertainty weight. + weight : Optional[np.ndrray] = None + Frequency-dependent weight for fit accuracy. + solver_params: Optional[Dict[str, Any]] + Solver parameters for the optimization problem. These are keyword arguments for + `cvxpy.Problem.solve()` [#cvxpy_solver]_. + tol_bisection : float + Numerical tolerance for the bisection algorithm. + max_iter_bisection : int + Maximum number of iterations for the bisection algorithm. + max_iter_bisection_init : int + Maximum number of iterations for the bisection algorithm initialization. + nbr_power_constraint : Optional[np.ndarray] + Number of frequencies to enforce non-negativity of power spectrum. - # Fit the uncertainty weight to each SISO element - uncertainty_weight_element = utilities._fit_magnitude_log_chebyshev_siso( + Returns + ------- + control.StateSpace + Overbounding stable and minimum-phase uncertainty weight. + + References + ---------- + .. [#cxvpy_solver] https://www.cvxpy.org/tutorial/solvers/index.html + """ + # Auxiliary parameters + nbr_signals = complex_uncertainty_weight.shape[1] + + # Parse order into list format + order_list = [order] * nbr_signals if isinstance(order, int) else order + + # Fit the diagonal uncertainty weight elements + uncertainty_weight_list = [] + for idx in range(nbr_signals): + # Compute the magnitude of the diagonal uncertainty weight element + magnitude_uncertainty_weight = np.abs(complex_uncertainty_weight[:, idx, idx]) + order_idx = order_list[idx] + + # Fit the diagonal uncertainty weight element + uncertainty_weight_diagonal = lti_system_fit.fit_magnitude_siso_ct( + magnitude_fit=magnitude_uncertainty_weight, omega=omega, - magnitude_fit=magnitude_response_weight_element, - order=order_element, - magnitude_lower_bound=magnitude_response_weight_element, - weight=weight_element, - linear_solver_params=linear_solver_params, + order=order_idx, + magnitude_upper_bound=None, + magnitude_lower_bound=magnitude_uncertainty_weight, + weight=weight, + solver_params=solver_params, tol_bisection=tol_bisection, max_iter_bisection=max_iter_bisection, - num_spec_constr=num_spec_constr, + max_iter_bisection_init=max_iter_bisection_init, + nbr_power_constraint=nbr_power_constraint, ) - uncertainty_weight_list.append(uncertainty_weight_element) + uncertainty_weight_list.append(uncertainty_weight_diagonal) - # Construct uncertainty weight fit from SISO elements + # Construct the diagonal uncertainty weight from the diagonal elements uncertainty_weight = control.append(*uncertainty_weight_list) return uncertainty_weight +def _fit_uncertainty_weight_full( + complex_uncertainty_weight: np.ndarray, + omega: np.ndarray, + order: int, + uncertainty_weight_type: Literal["left", "right"], + weight: Optional[np.ndarray] = None, + solver_params: Optional[Dict[str, Any]] = None, + tol_bisection: float = 1e-3, + max_iter_bisection: int = 500, + max_iter_bisection_init: int = 15, + nbr_power_constraint: int = 500, +): + """Fit an overbounding stable and minimum-phase full uncertainty weight. + + Parameters + ---------- + complex_uncertainty_weight : Union[np.ndarray, control.FrequencyResponseData] + Frequency response of uncertainty weight. + order : Union[int, List[int], np.ndarray] + Order of the uncertainty weight model. + uncertainty_weight_type: Literal["left", "right"], + Identifier for the left or right uncertainty weight. + weight : Optional[np.ndrray] = None + Frequency-dependent weight for fit accuracy. + solver_params: Optional[Dict[str, Any]] + Solver parameters for the optimization problem. These are keyword arguments for + `cvxpy.Problem.solve()` [#cvxpy_solver]_. + tol_bisection : float + Numerical tolerance for the bisection algorithm. + max_iter_bisection : int + Maximum number of iterations for the bisection algorithm. + max_iter_bisection_init : int + Maximum number of iterations for the bisection algorithm initialization. + nbr_power_constraint : Optional[np.ndarray] + Number of frequencies to enforce non-negativity of power spectrum. + + Returns + ------- + control.StateSpace + Overbounding stable and minimum-phase uncertainty weight. + + References + ---------- + .. [#cxvpy_solver] https://www.cvxpy.org/tutorial/solvers/index.html + """ + + raise NotImplementedError() + + def _convert_frequency_response_data_to_array( frequency_response: Union[np.typing.ArrayLike, control.FrequencyResponseData], ) -> np.ndarray: diff --git a/tests/test_uncertainty_characterization.py b/tests/test_uncertainty_characterization.py index 5ba0ce1..263e303 100644 --- a/tests/test_uncertainty_characterization.py +++ b/tests/test_uncertainty_characterization.py @@ -768,54 +768,6 @@ class TestFitUncertaintyWeight: "diagonal", [4, 4], ), - ( - control.TransferFunction( - [ - [[1], [3]], - [[2], [1]], - ], - [ - [[1, 2 * 0.5 * 1, 1**2], [0.5, 1]], - [[1, 1], [1, 2 * 0.3 * 5, 5**2]], - ], - ), - [ - control.TransferFunction( - [ - [[1], [3.25]], - [[1.9], [1]], - ], - [ - [[1, 2 * 0.4 * 1.1, 1.1**2], [0.6, 1]], - [[0.8, 1], [1, 2 * 0.4 * 4.2, 4.2**2]], - ], - ), - control.TransferFunction( - [ - [[1.1], [2.85]], - [[1.7], [0.9]], - ], - [ - [[1, 2 * 0.45 * 0.9, 0.9**2], [0.55, 1]], - [[0.95, 1], [1, 2 * 0.8 * 5.5, 5.5**2]], - ], - ), - control.TransferFunction( - [ - [[1], [3.05]], - [[1.7], [1.0]], - ], - [ - [[1, 2 * 0.5 * 1.3, 1.3**2], [0.42, 1]], - [[1, 1], [1, 2 * 0.9 * 5.25, 5.25**2]], - ], - ), - ], - np.logspace(-1, 1.5, 100), - "diagonal", - "diagonal", - np.array([4.0, 4.0]), - ), ], ) def test_fit_uncertainty_weight( @@ -866,10 +818,18 @@ def test_fit_uncertainty_weight( # Overbounding transfer function fit weight_left_fit = dkpy.fit_uncertainty_weight( - complex_response_weight_left, omega, fit_order + complex_response_weight_left, + omega, + fit_order, + "left", + weight_left_structure, ) weight_right_fit = dkpy.fit_uncertainty_weight( - complex_response_weight_right, omega, fit_order + complex_response_weight_right, + omega, + fit_order, + "right", + weight_right_structure, ) # Uncertainty weight fit frequency response @@ -893,7 +853,7 @@ def test_fit_uncertainty_weight( # Regression testing ndarrays_regression.check( complex_response_weight_fit_dict, - default_tolerance=dict(atol=1e-3, rtol=0), + default_tolerance=dict(atol=1e-2, rtol=1e-2), ) diff --git a/tests/test_uncertainty_characterization/test_fit_uncertainty_weight_sys_nom0_sys_offnom_list0_omega0_diagonal_diagonal_4_.npz b/tests/test_uncertainty_characterization/test_fit_uncertainty_weight_sys_nom0_sys_offnom_list0_omega0_diagonal_diagonal_4_.npz index 816412100cc79d1d223c43b807d91da5a0fc771f..5f9e61f39cf14446823153d37a55963fe40297ce 100644 GIT binary patch delta 3447 zcmV--4T$o?8@C&OP)h>@EdT%j2mk;8ApoCZoT&f*|NsC0|NjU86aZ{xW^^uYaCrd$ z5CFgi00000003YH00000005nm`#;o)8ppp+$wD@GhdQ_wbEtNIdlGn^Sqv4-k<0B ze4byPn~p~uoPLxc&JmG}(>@{IY=)Tz!`$0&FJp%W!#9u}%JvL6705pA^S{9XPyY}f zsUG6z8RR2n9m8F_c4!z%TX%;>w8sBY@&smNRg>~S%7j{_&)xa1A&7JDP{tEh|9kdmXhpF7=rgicD8mg3QjS(d)X0@!GgC`hb*GQp5K$%RyezS_;m+ zFU^^KVBuN;!Xz+N;d~W{cz9|572xtp=I50C7h_;+s(DBL6p;npA_7O z@@2(;*q#+NV3CDYcP>>y*}9>+Xrcy0sQ!(Efe*lXn_%#LUoBXg25&B(t%e}@o}jk% zA&8zWQ76e7G&94=4DKVaZr1-|Ra6T?GhJzJqz;7c#qeJ7L+Gp#yFQj{K=WkXRE1lLY_l!k7S$t5Y2wSR$|jJf)pbRGj7Eqpey=Fp+zb|yb4RM1P~95mD8l=Ck5tz;=(HSZgL*{S^Cy|D zsK|8dc?!ZLxsE4v;JLWlQI$t|?Vk%Ev>D@oL@Q|^uaY*8S zAvA^Oxq6t3te{xWe-85z!}loa&F#PrVNQ~qZa1>p)hZ){IuWIFlU_>pz-&)RVzng? z_IqYO<5e$~H+3>tYJ42u8usq3eLudL*h3V4=Hte+6FbsdfP{A+RHmseMDaHz)#(bc zvRd|~@>DlAE_)wJ{ac7%eZnH-=X!8|f5B&S!KesV&u^S}mg&PE#iu(0dIs>4wo74L zy&rdSsyQsdAk^$*U$5OFfRd4e(?{_W#4r{w{8TD{M0+Wk|J5_hc3&6wR$gW99`?nC<8*bW!-BuX`2}Q=qQ5!¬&yw@0Z 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 Z0000000000fB~A5p$!}cGz|a%005aGriB0i delta 3466 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 diff --git a/tests/test_uncertainty_characterization/test_fit_uncertainty_weight_sys_nom1_sys_offnom_list1_omega1_diagonal_diagonal_4_.npz b/tests/test_uncertainty_characterization/test_fit_uncertainty_weight_sys_nom1_sys_offnom_list1_omega1_diagonal_diagonal_4_.npz index 5b40d52af9fb42499ed94c588a2d7fda03abbc29..820fff5e83cbf9d238c74b0792a9011fd3124034 100644 GIT binary patch literal 3562 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 delta 3495 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 V00000fB~?R$PF9@WDNiS002xZ!h!$* diff --git a/tests/test_uncertainty_characterization/test_fit_uncertainty_weight_sys_nom2_sys_offnom_list2_omega2_diagonal_diagonal_4_.npz b/tests/test_uncertainty_characterization/test_fit_uncertainty_weight_sys_nom2_sys_offnom_list2_omega2_diagonal_diagonal_4_.npz index b30ec1e5a95388a754eabed0678bdeebc473c4c9..beb51f054b4bad456791d0d861217f2a7a9de097 100644 GIT binary patch literal 6790 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 literal 6795 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 diff --git a/tests/test_uncertainty_characterization/test_fit_uncertainty_weight_sys_nom3_sys_offnom_list3_omega3_diagonal_diagonal_fit_order3_.npz b/tests/test_uncertainty_characterization/test_fit_uncertainty_weight_sys_nom3_sys_offnom_list3_omega3_diagonal_diagonal_fit_order3_.npz index b30ec1e5a95388a754eabed0678bdeebc473c4c9..beb51f054b4bad456791d0d861217f2a7a9de097 100644 GIT binary patch literal 6790 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 literal 6795 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 diff --git a/tests/test_uncertainty_characterization/test_fit_uncertainty_weight_sys_nom4_sys_offnom_list4_omega4_diagonal_diagonal_fit_order4_.npz b/tests/test_uncertainty_characterization/test_fit_uncertainty_weight_sys_nom4_sys_offnom_list4_omega4_diagonal_diagonal_fit_order4_.npz deleted file mode 100644 index b30ec1e5a95388a754eabed0678bdeebc473c4c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6795 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 From 46165e73535e002d3e15672abeb7e7140dd631d0 Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 14 May 2026 12:55:02 -0400 Subject: [PATCH 4/9] Remove uncertaity weight magnitude plot and add uncertainty singular value plot. Decouple the plotting of the left and right uncertainty weight from one another. Update examples and documentation. --- .../7_sval_uncertainty_weight_left.png | Bin 0 -> 36229 bytes .../7_sval_uncertainty_weight_right.png | Bin 0 -> 35188 bytes .../example_7/7_uncertainty_weight.png | Bin 45769 -> 0 bytes .../8_magnitude_uncertainty_weight.png | Bin 39212 -> 0 bytes .../8_sval_uncertainty_weight_left.png | Bin 0 -> 26565 bytes doc/examples.rst | 19 ++- examples/7_uncertainty_characterization.py | 25 ++- ...t_actuator_uncertainty_characterization.py | 18 +-- src/dkpy/uncertainty_characterization.py | 145 +++++------------- 9 files changed, 79 insertions(+), 128 deletions(-) create mode 100644 doc/_static/example_7/7_sval_uncertainty_weight_left.png create mode 100644 doc/_static/example_7/7_sval_uncertainty_weight_right.png delete mode 100644 doc/_static/example_7/7_uncertainty_weight.png delete mode 100644 doc/_static/example_8/8_magnitude_uncertainty_weight.png create mode 100644 doc/_static/example_8/8_sval_uncertainty_weight_left.png diff --git a/doc/_static/example_7/7_sval_uncertainty_weight_left.png b/doc/_static/example_7/7_sval_uncertainty_weight_left.png new file mode 100644 index 0000000000000000000000000000000000000000..888db418e4a48f8e838ba97d9a2823d7bdd5fb83 GIT binary patch literal 36229 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 literal 0 HcmV?d00001 diff --git a/doc/_static/example_7/7_sval_uncertainty_weight_right.png b/doc/_static/example_7/7_sval_uncertainty_weight_right.png new file mode 100644 index 0000000000000000000000000000000000000000..4d807b24f3f026e6500db8d84ecf01f69122d287 GIT binary patch literal 35188 zcmaI8by$>N*flzUq=bTWN-8bgh#)N}C?PG~&Cn$wEues;N{MtxcXy|BH$yYTz}evM zeZTLVKMvQ0qcZb6&))mq_r315)+Xe&k_-+O85RTr!I6`dQh`8_eIXE}1q?LsKfFUT zE8s=Q>7|B~s;!9=)WE?Q^2)%;&dS!w%KQV3i?M^FxvdR1`!h~1kd318|jyLwmAgyX+cg(;;n1)&MegR?bvw70qmR6 zuy8F;H$u26I0xkmGLHQ#9=}kMuxqn|3qJf>%C;j^M0|Ntti;qvfN}r{; ze%up22Dwt!5@>S5|v!4BmV0pPl5Cu`}PT&QlIypN~^#FSghr^)AN-;nq(`E9Gle297V@ zW4G%}6UB@hkf3>gCAsp$k_ZFK^d+SQ_lEqV;EBBK>`Pr)Z0w|$H6B@&9~=C^V7quJ z$=rXWv;P}KH?1?jVjZ%#8!Aey?GEwbUsBb@@UYoe*y;OF+mS~_;A)+le|he&C!-gy zWG)v4asBlc)|;+TF*PJZ)MVN3o3X!X@@|zz zu|+1#MEd!qsCXtb$}yUFzI5N*0$t8DxT`sW4D2K)+E)aV;g=WsrBk(|ZvWygGqxJN zo)pbv0twCMPi-tYUy5?1n@DuJeo03_6D4P4AA&6CP0mz(+kauKGVK#YS|KrRfD!I5 z5o_+;-qtHC&%(15U&sYLdxFE@2gdO!In=z_*0y;aMW;!x1=p&|on12iUh(5+W3zd` z89W}z+^tY&*NRDFMMdLJ(UdRgdTfK1;a&X zjR!u3OyHxcjkmj{O-{kTF!;FxvPNEpHXJ{sFZcsRr{Ug39i&&MAzmWI@Z3(r^rs}^ zQy3Qemz_6+Pw!Uuz{pm_6G1Idjpx7B>{I`9#3R^u9@3e%D*9|03IaV>u|&q62bC4v zz~Lv`k$(4T!2(A$&dj;E*-S);q|lP3eW3UGw|Z1=ZzkN$jflCp#D9_OdEP}eVvDRr zlstrPa}$E|Z4o=AEM{$}sh4VKWI3l)=Gf?Yc7pug{$|cN(n1ZaUSTBb6($D8cdZJW zy!U^)r*-hb*5B&h^gWNNjuGYj(L-t1!vEkwfA1f;y?`jsF^1)dSPOE9Qkqzb47Jyn z^;D0@Kb8~2KP1uF>yA}$DFhaF3qnBBaW?F+H@>(&$JB2$UY6atrp3JEno9Vh_GEGS zwKV%f3AOiQcx)`}hJ zho=xWMEMM}^+wKj2)^2mo5+3|Z9xiITU{+S=_5;Z-+FrXEUfLte7%=u@rS?;M+Zm8 zuD(8Jab;y?^hT3XH)oFe->f(5KhvV#GR{JmaH?Sz#pzverAxeazla3y#{y_RkyFQ> zMNrc*8$LjN4MUHIHY8!@U z*ZTHOxqQm~OG-*YDRRMsZMtsdWV;>@$;=LnjEsbYg)PS@us>#H?F|#nt+wJL4D~rXze2`=%wc*dOJR#1P-lWqk z?#BiUL5uPa`fuiP)j9fY-gd*A;$FtiKyw;4>Bz`rAQ)T@m__T+9~(U09)#lJ^qi!x z4Y(7%WkYGKcHUlEUT(QYT;8XgZ}1VvA>-M1jcjNTzrMbH8L5@8hUoCdx*+{mKnK0l z6?P(mInZ95KKwNw>4j)mvD3ZR?#D_*PmjVW{`lK=4G$y3w8YyY>f&fW>`nZR<1vrP z_(ZUAzsJn82$~mN7-}3)QI_KRAzk)iFisV&Sjtd4*tIb^N*`xfE4o(sXe%S~;*h?l zrBSB{LruD37_dn=A&~45@Hd6Zo9oNEX~Em7TYZ`r#8L2ZhR4ha7duHVir5?4pgprk zWhs5u#u+}$;AuqZ_Z8McO!X#Y|IDTP}TW&5=mB-_IwK{yox|uye>OXHc+sk?=r8- zWT#Ul8smdda2a8s<58_lnWixa3Vw^E6h2&z5+{<_lI`TaCM7((-J72RD`l#tEl35T6;G*g zw-t@P2jZ}Jul*~D%+9a{kZzD?d~OhlhlC(E#ZH$`5?%>9{8(kMLzwF07)gF$HYB!M z&0%?OXdkstsC4=yx=c#r8Z8fjP2pBxSgcd&>|q=p+$S^-k=gi;hac^#C zL;lciMPiwX(~57V{*xuzd}v`Cp@m!Wd#cfbpJFFZ-)%p%-;br)8M4kZKR1>|Na(Wvx+bjGy?(cK`jc zbN=~}^VnY&xpv!!-+~R!w+r%gJ<7K=VpHvgMh44>L!N48(7RK;6LIC4Fr;ki?uDO7 z{ksT7h);9;N9|ShM7?{G_4h2F{|JbhqH~X9UFPR+dDj6utDrfXv>_|jS@?PLajj0L z#qD|Ffx>L|m#oO|{Aa0ZcOZ2XyNi4=%u>hsI#CBA*??vVc`dL=H(w0zL(KN2qUVbF zR|qu?$Ac>V(Zy~VyVC1nvi+MMv5dHzp9w5D6a3SikVj!@{)JOU!^W{&u^f>9@Xk(x+!AbUyrjcH3ifm zI|~<3hYChYtY#+jG5MXW!>uReuS~RYLJB4@a`1@7kk)6A#Zu3%*aUtH{Bq3O_WG>> z>ir!Ymp%Dv4-un=n0tIzQXt{Gk4)!_&*+)4)2wkN;+=nc|61*4^MDZ zXX>Bh!(6NL$jb;rO0fwDla|zI`7mEn8Gxsp9@;kkFb}y5?Y3w5omR^V-x4npNAllp z>T{%-;l=Sd48V(JVPz3Knhw*>)S;* z^3YpQeJYS|WgqX9LxoA5YKEn8JAx<%j^|gz`WpI!kIaTCpLko8XEXQ(UMo8k!n7yq z>r8cno_)x)B#y#I^Lg$1zDns6_0Z$%vM@{zZRxDgTU2|uopY$vYS;gU5r4l)8oaiWp#l<>J8Y}KQi5WVyq6z z9cRA26I*N{c!dN|zU){z!Zg9FaNEAZ^s&+0O~xgK{k9RFu6t4@E`Gzlr{2 zLJ?~F(!rFg@(=C5Yxz5{IDuKvftb(e;UUn7sbbwuSNTEAew;Y2_n_hF}^$F=9|Rkdb0@_sSGp?x~| z!}~nN+Ip1N)#g)6E@vCF%!!|9Sa6z=Nyh6i|7+&{D4#xwyel^c1t?kzNl8uS!7d-C z1|9AhGUczdIhJZtogl!l7$t15bMb%Q& z3fIy~h3e5rkox{5;8m?F3ab=!G4K7fLXx^3iLkbcpf}v?Kh;Hs2IXde0$y`LkthTB|B|Y}cyo8FfM_45e|ASo#-&Q_h6a#-YAbx_@C| zNc*QhqWjB1&EscZf5fVlfET2c4+)|_vpZb=>ysVMHFyKFd~Xw|XXZ|TIlsPuLl++c zqgTPSf;*AiDqW6in@IljAFGi(K5Eru6m?qsaBo>2F2MV*f_a0)m;3m$Ge-8J0*#v$ zly9NE_QsIfd8Y3$RuucJzLp^=QFLmL>O4IZG#-Ck!YX3^d+-*6VB6r9rhI{CMF}nr zwC_3T&mZFAYyL8KWAgYi@#};258k`wbIHOK7eTtY#v>@wFA-nBcF{h;K@Z6rHHsRJ zWFD9PH>tfU@%5-aO6{$@ToqkY0YBW*r(hTiw4L&~7e7`@>XKqayMzA?)av>odeC^l ze-Z2Y+M(S+bxt*7?L1zKvK|0p%^5mR`BMQMG9*CjMUTWML*{Wi)+a~z8v27~63%=w z9;g*K!?8@R#_y1GgCc{Gh45XX?c5YhRGc%NSTr3#qv0rf#!untFZoDmPhd76D%SYQ zTOf~n=!bXfOH`JrWuAW!Zn&o>fTN&=!%`e#KJHGhCm4ZHg~ox%lDosYDqU3B68~Tr zb4YMh%<1{7A|E;kdY_$*i%5$YU9~oWn0$3z(wEeK74WUM=Q8fzK7tw|(+9EXyc940 z=FCjLXn6W5HBkEMFlDc-q|)r(E$ETA8KVbpeCVq^60dbAeGo;eWq?V2-klry{eRxE z276hwfC*7#ow4@#@@MT^VB!~OvkXkFzI)=e6D{J`%{s?WVWSB><03A!Kxckj*gT*IJyHkHhC{|22ad0hiC*SqH6v!|F~rlhc@$~O$rj6FBqmx!BBb4@-g$ySXm zsowtlj_11NePtC5JE)H;O(uFElg0ZmLcP+7U&?Qb^uBR<^V4T>IStG6ffvvz-l1$8 zupZbOk5t*y>ce7Ly04{$OW#I4Ns#?}ki}y~ z|8_`MMzJim2;b!#jMxjA5$DB}dn9}i-C{h0AEo#0M5bB<_|CM3bEcMJMA4ihSO2Ae zPZ8Kgvhe~8o^QGC7VOVtQ0v>d)QV6j7ggJQ=RN(%bYYlqflI|xm{F8JgHbov?2iZ>Pb8Gg-(|;>pB4aK?nCW>JP8o6b%6*A0{ZZ6%=sma) z2b7;~0%zulFJFFDpsWW8$LtMq`yKQ>3^E_0VjxsLJ-8U`? z5}!e_0z1nFW&(QLr&H<7L$^GwEF1U11#Wn`cNq8QQN z82_fB6;OUWvnMz>XG$*AmtFSkFafkLZWiVAqz%FyIypvB%s<~NA@N)P1=WV?NvmB!!w*EMuB@4Y@Y1Q)SN(Q-M0mA^cV{y!ewEYta@50Vp>uB8 zmu_Zezlc5PKq2B?jq26W*Bs~=51|MflI2IKqJupwhe0_@lkvp>jhz!dZ}IbeB*dR( z!E*4s-zAHHz~miand1N&L0RQAHpPY9iIhga~aC(QE0jX)z!Jp;$PLR@^%EZR*Trc}LnH^Lj=uXNrnI%su6yoYeQP7Y5fe zlWdW;6ZFL#Y{8s93K3URaqo<0p{WJ72_`4G?MD~eg^vA#>kkMC+h7eh{i@2MZGiLa z2K=Y)j^Kd!jR;5W{)^~kJ+Bi&2?+_pDIKte@)g1;e6YzjsdbI36wQ?cHDqk^Fp89_ z=~GT6Pm-yAC2iTwLd0OGKMFfzL0QxljNR6y!Z2}xv#H{5KmRzkrl#hMT?r6HYCDonc*WHT7b9JU@)$29T|lmwPo`ot=&I^YhD9AcHP1EeSYne7+2R z95`jg2M9>125&^s`~11R$Wi=iSSz{3lGj;NIOMke_)1ps#PDZhVx{$5 zoXs7D9)mT}^&w7?@1ql`rQXzcbg3p!z(2N>ZNkY+gNiy`I`^N@1-LyTEJ$weqkY3D z(f0kt7UjQLZbxfWQPAyioSNDG{uE*JvEN@i_yE^O^-(f+?7iW&Vd0biVf%jVo#}Y9 ziQSa#Vk5RC!1J6EyhO~<33y!#2NJiokuIS@ytN_78ZYm8-G7ZBQSsq5M~GOLQ?3lX|m1<~C$raZ;%k2x1Yk{5du z96qNkcQY*-0yy_vtM;AxJ4Og=4k3%$eh0OW-Ox+ohh${kp@b~1E73BQn^{Sp!o!bU z&OrLCU0Y^pHmg_>+ak}ga^%Y>h>mRIJn!~?SlTzINHP3lbb{3;IBG8cX9z~gFb%7( z!E6$z*9p~W<{Dt{EPnn)^#$WsOyY}JPxDEgnbA(JGK1SUC)7= zI5Beo&X%w{EymaErHx6=8!XQaZupj)AlNA*cVOOg_l(OqJe^)3$w6UmIsA{eVnTG^ z;x8og)cS2=iMskdfF){f&I&8lsLK z+3{gu4bRkauamq>c7C(0bfg`t@6|szuN$I@OR%)x=YDoo1eRJBMUrlSf zzlU+#%gT7N6$}ojmcsxi)^jS6AwPrxlw4WDHL`EqRnw+a9Pb;@Q0j~II#q9GKH>3E zIfu7dbhTQ|r2OFb`qZE|WUZ{6rQh7K^PwE7T5j8yepfx1t(H!9U7IdQl{FjC77Q;} zMa&xr(8WZIQ>2A^lrX}ZD{UNdK8im{+(#;4o%f&5T>I~?WdK-X9!(sT-*^OEUfpX{ zC{Ie*sYLb_i{qWyI{KJ<(1=GPlGQpferj{qTx4Kf7>p57$&$%cuG%IOiBt7fC7qi< zA!KC1-}DiKWBaua?Mybu5#}EJ|B4`CblU#GL>!KG!=g*I#`W4039AhRW)BMc_jicb z*B-Ad#c;2EW$8ZFy6^sgIc9pN#V~q4nXXtH{x>T_Kyt1o)pDJO;oN45#5XgEXY@X7 zMG(GQarQGeA8W7on#@{hl%=b&T>|-}J6E@2LDBaNZ|`i%SD$)!#TneBKj| zqD|+hr=5JiM;EEF(t>tWF#YX>dUNo~$nDKO5nIbD8ed(#Qp`J|OyN^PLs=8*U$&o~ zU9t0YvfbOYHsii@Xbgztm$+XJigoUMVDT28P773WiAK{dF>esa zIxCe|Z>oIhv?>i+*mzFm7}3RXkKJN@(l7A|8&W4bp9eXG7+!3N9t&}W70reN|S zA$vD5cozcUk?U~!WHKeIBOnU%qcB7s{>wV_p06-;Zv}BSZ~C{)oSu>KLt@q?uei8) zzxa*kUiCJ6LCu~JD-qjc&@w#D2k^!rf0ejhL;UezC*4q9kEmNzk-fLT0@*jM|Fq#) zC-{crLKTQgFkgY;frN_e(`YTBc7OSFfQd}u^~D~!xdK&zbzvhTSRTG zNK9<3W|2Npo%_iW&?YjyE>H7x>;7sXKx3SfY9+{OEt)ie5ThCFTsD75Z3xG_6v1*A4r1ur*>O%+*k&uG2)!g5&sC2K zn1I4c=pV-#f8#V7)_suBVxDHp5LlIj4CDC{TplS=A z)vymPO9(74;n%5yi=s~4>{388K~OqA-gv5-wcm(>x4#;%hrBb<2$=MeNQaX&jpu8*?Kh!Uoy~cC6LMz0g^$ba z5rzP?<<9GmWkkG8BzoEsQ;8OLJ#hjLRhKUt z)A}D`;M@``ERXF`lO&|l39F$-iVFW~EbDyQhom>%n6dqTc?69Js;h4(>olSE1aQzgG&Q^VAc8C{U!p z#iw`a2?C>ymlrn+mKixs^tA>gru>;`jL`!+n%>zam#ruO3Uwwh?xBRT8F42_`lDEhGWWK{Z1CbOj0`48rF1)@&^b1dH zVA4TB83TM1|5Q=5_JtFY?~++yZ26 z{%G^Wy(D>1k;gB#W#EhUva>jiTfTeu^e!1&K9X~`w3J&q?!i`dVd_twD9qv z`Ox!hzS+^T0aw1kCA|EiWmh*BeUHMFx@~%2vRXPz99+qwTgAdb~)kafM9bjH=JG`Ldy* z&}90zhQ^=r*R<{XPjiuE`%3H$D;V3D40be8#iJZW>m87#c#q(xw-<$bNr^mp1aaA! zENpWx2_`;5xMGb6O*x@s;@@odKFv!W#3r!>r7M1wmiX6jjBNWZss4nFI@Z{MQ>_JFxJdbj8b$8)(MD`aF%X#7y65`HteVAedT zspVFtr!e=vB!80Eb-BR0!#TxJxo$hC1(zG3j=jXDBC^nAc0}(Grxf7xZPxX$w7Y=1u+y z#A;UJin(Y%7}%>lho!>x^jKaW8pm|RaQsI!f8BpuO`*>F%F+t7fUV)+Zy#=gp zMI}R=xjk|HDc6bRTwRWGkXg9BUv~eVggl@T5J;%mtIjRCxaFp=lX2meNLSa9miO58 zl5!WXI=;V=QB1MpW?)ef+`RyD7ypgHG}MpwMcgyiM+{W@*V zDM``>6-^LVEb0X-wMU_PpWnY$QzH^~*;xRXsNi!X=z}|DVJdgB++T}wvZf{;ntZo^e(!$?sAYBL zn9PM(gO(us&CK}2-HQ2`Z{Ic#0{W8pkB&!_9otDwi$J)^Z{CJ+2q2XM(-%+piu5yB z;P*e-MVyM3JVdKie*#BI_~%{9Fq*ofqftVPI~Q-~xg03XZlhh|dlA~x_6f_tFeUic;V1)VPdOl!AL?RaD8mIp|7 zTl>aiG5X2Z7owt~GFk1+LP|=CO(F0ZxQ&JjbcAhBv;bZd7zv>4S%{J=*qpnvuj$OP zo5R>}qJtCaHt2FLyGXp?f*cXV2xiMYdL4A)eIB_3ykk{VdT{zyXXl2uF-B3m>6fWO zr5Y zJKf4hDd?kC6t(#BhYc6JvJLdO7qL2fyik^MYIcXJ7GoV)JV(Qd!rYrr!UMb)8qYzI z!iFB08ehquv+!ZA%s39Lg75s^dXVbBC^{hLrDBuUBTW}GD0n{}G*?qf zFLb>kuP4+8&k#yPIVZde#u69tvGv^owc4*0g{FY0n7FuyIT%cRw`xlcmz7|VTcl-~ z?ErO0luShsJM{VG;|CAc4&ExG#__{mnSwysd)VN=HZ$Kt8SvHpp3S-gd==>YHz5z) zJd?)mXxa5brne=*Z^drAM3q03*TmzBe$INpqcrg>jH7H5hIN&A?DSHZ>50H!mlNTJ zswfX;*phdCo`lBu!--K2MyFcLzq;x>dsKB=F9{wRWlCs3TB`YnPHj|jZfQ{2vE`zc zdGY40D1*y`0Lx2?eU#6&jc8`+xF=)CTyCD*qwRXxzYaIkYwDd_>dsBBT6gF1JW|l% zmG)7Za+bp!ikB10+E%yVB;*Em>8Fs8$Mp08yVE-FM$_EZNEww=?{ZmmB7?X_Qk@Ag z5SpSc2S4onIqtolBD`vI@$-vai0-Rh7Y>I(wSY@Br}I1wQJ*XOZ1sT;wPz%%W2(y` z2F^a)4RTL81$@t?$h4+=AekT84)r&%lmKe|ca8bA%1b;2qL<9?iUAo4dYLrui>HOP zhgf%{sOxmO^)~@~+KPE^k4&fA3FlB&Ep2GMY4Z{jcj&2pS=9e^G&=07;3-w_K~2oG z2QCfUw9;hxvfu2Evz$|SSR;>tQY{Nfaxq^tsU^YvIz5wuk4p{$AR5EsP9*n*`Igk} z`cnE#gb{dE8>EvB4}lTY7jMQH{eFQ&?Pc@Yt>=}K*7vc_(wF_my6RRCIB2V$nGe%n z9B;OWp3N+a&4CSDZZrFS#vCMS8`Nx*?dax|`lD~#r2+bu?6*RUSjE}0f($h-L8L+O zyclA{%lrDdrbT(5VO1OfA@;S%?#dgx2%W0Ds%ar=wO0cFp@1v@qv;+Pfh?N7MK?%ZPO|T|5 zc+6`rBpsTq70tlaDX;zZbz5Y4CPVYirFN8u2!!o&$a2*4tfwtsVn%RJp273e1R*&j z)p=a=3ed7V=EE6-2c+C4gE^|%Sg-^D5tYx4m6yU1S7|V)_U@T*eGq}KcyhkmfHzK0 z@~$8iEg`SHYzKY>d{y{a@p9~<&ISqb8+(sUp`HC%%FnS_5&eE)Y0hUgfIO$217#Er zijPANTgB%uUjjxpjn;aqGeM7umD4T z(A|lmzgD}rmtac}Rgec|FEddc@%){j5&8b%Y*>en%B2v}@S23@8%~!dFhl^cRPN6k zD~M|!?*u1wPg~dX;HCe^k8a>Lg6qkS02i=>jxD!W45SL)B6}HpEtAOAzscX=@XFJ5 zoi($8+|%tTGd}cCG|Lc~k|y8D&c+u=+NISJFJl-(Erplo_IM((epe3F69BWDx7*lB z<&D%Qi~io#q_Vc8w=7KgD6ZG%>!oHxKfAYpedVo87Up*Lkki~q*`Jyc_tNe1<L}~NGj8bludq-Gjsoj?EIqAwx6ZmTt+grsBGD3RX+JKWze}%v-`~9lf-RfP z?Na&$k+@Rl+&m_di_rFw-#~=+_?Zo#PpO|8(>;mo#6&jNowt5EnM^7IDV&$Oz-Yad z+ErzF5mVRBJC@5fmr?!n5ky*Ay85O?ds2qzDW|BmuL$GPwC?Q{QH6Qdr9(+tb-?4?B*e@;1o76eR6PU2#oaL(Ip9IxyC z6mT!-`fZ_vSD@mH9QAVF^}{(r<@pF|r2AB~*mw>JI}U>E!5Nfzt6H5 zbcGdk#770AS(6SztPdE+)P6wry$(sy1jty zRdK5WunXoWWY9Lvqj!un@e z0TZ=eHtMxFH>|Cj%LAwP=ND@P%^MGPXaARBRo5jYi*|SoHdFI`4!aIY=>E}w_(;AM zKja#|jOTrIAR~6MkuJXth&R^|^XBd|{h`uU5*L>Q9FpiWnxww0g7_%jqdz3!HRY5O z&XM_=5`zGe9^8jq>Gi+zM2tnp+Q86w_4Ct5$=c9-TY~YjUnTL?o{VetCJPey0vKQ9 zdF}vx4z?=qmk~^HQB)sK$a9OMYoj#r$Uf=}ym8MsA&4WLv?)H)W^}3u zhzHwak(BIl5y86(U8VOpMYfULtrUS$@gDz5J|EX z@Cyr5Vc;fk;2{;;#S6Y>93bm2%8SmkPhIq`;@h!%m;l9Ss2cb{!l*ZSxu1y;`F?t@ zL8ayq`7`H@*G_M?0e_;9E*;@|GNxX2`qz>rLlQJv*r0_3{VBKG;RTw?V&sw zk6Gib&{pqtS$V#eRQx{w>a5`=|Fc-gr%^M(&HYsxfn)xFwk3WmMvfKUUpkg^J5^3o z2e{GFXynO#ZbROs<6^(23i%msU!W1SD%x8*}k*mq1Ltr%`9tGk998f zQ)TqK#dPNFP9mBQ8WtT7i-^p*J*1S;oI(#?z@)nMKnZkrcmGjXR>lL$ZuiDey7^Rz zvGHl~XP`6<0{Wza8wM0s$0Gus3MUgo870g#n@Lb;2E2_y;HjbK#TZe z85{;L44Jz?h3IuewiA(|kESlDTV#L8_62lfD7{}omYlXW1vnd$yB|r&Bq&(*dLO+q zjfshg*hAOGh8^god+VE3t@-zNu@o`$-4 zda>@I~uyKi-EKq#O&I#uV23oth0SpWHv-= z3ze{VJPRe<4o8R&y1*tNBFP@aj`bDDRlZG&z1nx@Ax``DTwqQm{&hvPVK9V@;WG!# zosCwh%RdRHB75+eulK$sTGL(WBUg0z(GiU!PQ}MlmJ(^R(|_K~85lUmkBy;N||=a3_fl{eD@nM5hGoQGfSZSc{l{OZ&LwaDjQ% zxz00{0O^ic$LZna>2A7ioqJ$pc4nqMkTJjh#?=QCJv20wxnXfo4Tbe5JTnxqTkx&1 z1!q8LSy{tN`-O5RkFF4igR?UZhZ&1MTRvRb-_xSOsl*yA2e@9s+2>pDs=3b;ckkxc zJ#g4b=~W*b6~5`VMR|=9Z2JIeDkd+~6sTJ;l&14LIrQ3Nee-V{BM8UM;s?izEy0+$ zxTki~=aY3F{09dIJ9Bm9!7UP=o+98MMn*;uJ8*W=Zd}v!nbjocc%j~avN)m@Y!uRG z9|Afc%2HC1#p})C(=@OrJbOs-wc1Zwew5RT?&zLA&2vTf%>eEk;PtT-tC6jzrSm;M zL14G&!}k{h{h5IeD+q@iTuCO-c>@69)e&WJXK~nQ4#C3v*vT5Gt*57F0>3R4Ag4J% z@n-@Y#_jnGVm;5|<_mhT=~>-zPunE*A8DUGgI8V?e@;7?{)etDPT7gOFnO-d?IGUz zKHO}7acC~q=gl7MDU;tkDqoR~NsiOv7O#;-dWqb)Z#VI|!aOk`CIt)pW6%PfXxhfB*m~sPmPHq|p2^QU_`|<}kiNzgI!Wdc?wrWWHvx7ZdjcrH6A=nWE%q&c_RMv=&nc9!7J$=fgeC zTlq!W`Bko>UfdZ8h4YfExzQqv7X1TuP<7?O#FcY1uHG+7*G z2TrAocXc#G^f}#Wqf|hzu`H-+PdR?3-w3fimXVU`RydS~pEyIxQ!nv7)OjEyPEnqO zWJu&QO)NTjvS3~w7CAlxz8Z-ByxTyDr?~<)iXy=oH2Uwa*AO`1D%?B)PdgSqf_X%R zHiBe*F_L+c(ksT)XIR%T#rtbb=Yu@a({Fs(D12=FfcY=fNKm;CL9StFL1;5f1zE7Bt0x0%obNuLliL6xe*JJdc5F z_4{*LYEUwd*<>_MM1ko}#@Aa;A6UOe;qE>tLOcsg@Is8~KPhx37!`k0X;3G;VZ7$Q zwKDPm9lN}|tXXP;1Eh+A-fKXgsy5h;Y6esNubM#R{lC+Iv1$!;T0YtoFbkeBxc z7$tt56ma~i>H71n$x#e1X5yD+M9LP?)7%d-kd7$o0Rm-_CZC&2y}zIYk6ExCyK87@ z1VWX7>u_;m0!S@gCiGqt$MlAMhh#cOg+Lx0f=|_MU7|v|bkf!?N@27@qfwQ=7XEu4 zG+)4uqkdOT6AO$?$_g`#6*8|7Wg(x>UzjFSR23MQTi}&duVGahUH{^U)`>r#b*vQDpW;2z= z9@7E@l#co6 z6M^OQl34}dq>e~R<@c8Z7Js;mIvEtv!63B@Y5l-6E||zFVEEM(U>n&4S&uXzXI8p%zTh!UP~fnv2*$;@oNE{ z8W&XkvET1%0o)Whn{k}nb&-xBYXjxs8>hjGOlNTJB+_0eD(c#~E)1N&4M&{14y`A? zy`6=YG!d~A@`0&19SW|N%Dv9?9WF_l%P0IUmMXg_I?>%=4Q>U`InSe4x94fkid}a9 zJ|rja;T{la`TZ^z0~dF#xPwX+Seut{Fy|YuBNnJ}*Tvg=+vEa6#U|*1>1Acp_GU#i zjbC7(Brx5CreY=HL;TeD*Ud7{cwlxE!0fzufecE-Q-@k0T+!3hLki57Koq^xhDk}a za%PA!Prse5Nu7K+cz!`fAI9<2b4zWyjOf~VSOU_wF!SK zpvGSr=;;^i1a2!X1jzF1^_P3O$*I8hqu|1Pwe;RSc4qSpMedcQ)zG@8rHiB2QjZk; zDspqVV|k6hND6AIh|^<5lw$?L4#2Y8Xz@H>jlTlCIOMi(Ah)UX8ytKM0ZB(q5cJmI zfotyz<~*1UaxAEin$qF2cspbYZRb!DY7_4n6KgQ+2+k0LIa(@GgQi^t7o-}b7m|{o zE%9K^TZz(Cp>{%PznU%!^1-?%7%-r+<%!n{3{F`!Wp%_pI?Y1QdniOX`3X>G3i0t0{@##~gQ8ooN zqGlhnNW+&*?RYA+hxg`+8lFcpaJ(o19>$@%{dt z2K2tIpmS{oW9$L?xORvRU?3LY8+vu6QxDw5{!yIFkn4J}J)iID9XUfdAvrULO#>Oq zl-OG9i^m7+Hq+@>M%Dfeu~v!a_#o=aj?ROzkp=BGvyG%(3@>KIcyh_L5!qmVzk~TLx1KKXMz{mpopgoG ztn2Dm_5GtG!{H1$9^1Lv+lN-EAtXU~7~L3O?f1iYe{MK}laC?Xr$;%?BG#;383~Vp z*XZ5>x0UZU_o`3omH?RUuYiY)q!LZ+?Nx-_@^uB(TgSe3K)W32J_CyQ&*|q$mNUUeH;>di#Jg(Zz5NkK5})HG zq|>ReyF7wE2C=|%BX6I9nG4x<+PdQFMk`378F!`@)Rl}T>JBcSs4+zD_{+2uC5;QN z2p=yT-+)fB$ zuW%T7#s83ya5bVH^#8h5q(w`j5Z%7&&`#$|GwSsVEp1ML-$YZzPitg-&g4I^dH^sG zUNYOoYRZ9KQzwh&_y;aR%lkZ<*B4fQ$qrhjEE$ zO3pbWhzKHzfMi8L6afXvl0-5{k{ksU5CzFe!9b88L89a+Q8q}<8AQS+2z!&?SazTP zIrqMLukNdQUD93M4V$&roNLZ8$N0YA*m10TWuiPG(&@Z&g4j}69pWKdL)xs@(r2II z_V-1e{JIOm+opvd$rAf3Mc?4pT8$L*L%}X~{3fWo(=j-Xjl)?s)WlcAAbbw@>e?fW zU?zy?jn+(w5;Agpepn7(8!)vRdU{6uK%r{*qjK**j@DNUlKdLpbOwZHQ=4b>EgvSG z1%=3E%C^I;Bdz>LL_N8Jms2b4mLiT&}A+7!X*B>cir=6MtfW@L{H-w8XcR1 zcPACoHa%JKI)D6_2Mu!uAOVo_BJ42hT~e3=Sk2_+<Y92(>9S}2Yuws8LBC~j=M~c*v^Y~*Fzb+nnyaT`JuHma-z-iP z=|c8h>qaDM9b{zhK?@GA>09%TxI(+(%MpBCZzYNvr1;>jhqg?16;NL*C)9EIT>YOW zj2BE3um#@j?vb;sO6(QE#Y;?l<9|PFLX!to1-Yb>QHYtubB~FC(r~5WP0KvC^i;R) z&&Oi1^0*_v)*%Xd<6m2XiBAV3CCu=z{$MV&EL2vVE2J!jr1)iTcGgj@pcIOyqNw8& z7j?e%d>tdzy*eVgN5D%O30Q6Q|3UEo;Wun*U*#w2c=_V)O(xqThWbg;e2vVFR&)&GRUS zIiuHw)2COH#Qr#EO1G*#Z${TAW#uiJ7P7AF6SmzO7bY*KmO=fua>bgGzOGX$_|40%yCY7QKD1o6P^IAx`U2+=&$8$YBJeC<_7R~d4|lB~ z5@Na$&)|puK(m^FM)rle9@#aUxYsO|qFEKTjEXk=NEOuRNgIwscR`MdG{K5RnO!@A zsmLh$E6IW!;VAvLltF8vLq2)ULAS>c={%Y%TP{rHp8nMS#gsN{9w~Tw^pw4$4w;DR zQnU26xY~6pTlu@)V?sfHeC;ay9oF@Ype>Fdqlf^^#-EsfwZ;&+^#27OIkc(GudwAQ zybRr0mR{Stw}cndi??B1e{AF&>xO3qw)}0vK6}rfnmMXHHdKhEcSclTGU)Anux73M zI#Xay-Ielto|+OkcXDv6)xxn&rF$PP&?2o;Ot6J*0bK0C2Pdjll}cM+qhU?VeIdnHDVm(C+=Bw zhqrbG=}xre9-wS6lDd-gN4ss?V>93=KEWVER^N=ls|ERDAxY_GAKe!?>f_!0oDWe{ z)*kX9_e=j&U)AvoAsFg8qDs;e#4()fFrLP{|5G1le&Rs%c!*{1TJQ(QdgTxsxAtDk zJXL1~pDqfNVwx9nzZ%2zGQrJ}jj3HCiS7k-Xr1rglJD{~glek^DmO*k$gKQj0A0{0 zF`Jgjm+>ev%DPeb-2L@$xBCTXBdL*)RpS=q8Cnmq?p?|_LO#NaLys*mFMLTQ#q(wY z&v^^k;l68i%};6nt$kN_X$|mJXoU*(f;<;H8%U6vbEjIKik}XdB(ZaW8!8F2;>?2U zudBt0EL>i_B>os~=Wah-x<=9}xgxp1K)gsE{1|)9!!Aom=Vu_ATSEphKV|VuzAJm@ zwjp&(JjkQpPv&md^_WI-|6`QDpR@EQ22R26bbnlH2u`Tw=e;ME4eJv`Wa#H_xSfA@ zI$)_d=v?o!IkG(KiDwQG3=ZD|=zXWgM&Hk;Pigx3uT>l6-&JV815Q+B zb6qj^ujJ7*J%Q+`{aBfh+vBsuII%l&HKi1spM~CvbZT{_pJb{|{{Yt~iV(0OiFb4w z&g;Y2a&T_VTvG7Ecza#%`(R(-DrA>Pa?3WH1G^Ed9e*SExQ@xUuI%9n&?M7mN&FpJ zp57++>0aE_BNZG$v9&)_2iiGCFvMB%L37ga5mjktP>=y}mDRQAly1}0Z(V;_=dTJ} zKz^F(?@tFb#RzI#-uztYL~Y4xs0u!|wof;34j?Isx~LfV`P2UNByWLzp+Mvrsd=0& zy|IYw&qZlfBdK?>SAKB z$)H1liv++)$nDPia2*R=v4p1{;RX((R1L?$I4R+E=Mzcsi+J8=NXghdA*8-Bg{HBx8OA(LvehpXw^Dc9TB987r^68pZa-WM z`}v>W93`xJHHq}*5x^Fv)@vPqIp~np#2B(tL~@GrkRQ7^a4!t@FvdL~*Z(*?{M)T& zVl;q`i7dhMhC^T@el=E4?RuqONS_Z(xU}21lyA?zQ>CAgOmkAx9BkE1fAldaLts`+ zrcLXSgeybZnj3+8R^?PHd4V~-zu#R$H(ll;!4jcyA7`trEnU*Oo=uJ42kinBcYDM!}%S`c3D$Dw{Ccg<3ag_O#XON6jU&mMl&6K>mb$tp5)? zZp8e-QNtyc;+8KKs2{03?+h|Q^Lu8V`1Ihq(Ic8cY{=QX{LdD77`~OSJ?4ErCM4Su zP(q8hvXW>p5_!4r!trSJqGiLyj%?M2rck^R#d9cWw=Ca;c%B`Z$*ctli)_10(vOufw~Ga>c_X@i;??lPnS^BO5i9 zgz5+Etf79d%vJ<$TozFKJ#T{ z`$pPAwlm%o$;B#E9VnO8M})Y!0}08_*AUkzQ)PJCUAlj1QZ+NUrE7MPPI5tTJt=dM ze9%2}7Uz;#R5yDtxJod>CbcGOA-6MsZhhSCpuBO-YwfCAr>WGAG^A-2RL2`*F-WoQCiQ(GJY#V@zxuzt5>G|#))NXre-mcYqOFTd%ED|@HsS`^gY1YZTl_Q%gWcf zIgRyHmMn`J`wxB_K&_LZ8XLm4$~Je~$H&HMF0Gn1dO9yCv8th|qMi!$bsdu{irh1< z%)1crL;2h``e2>tDOuesIc#@^pJP`3_G+rPRh2E__Kx4lx-i)@tzSl;Gf1_puO{*A zq|Z|>IAj&nUYs{?c<;70*%Er(x%n5AW*>V@I?r`1GVlU@UhC$6`6HW;rCk4aXW}>- zPJiu<6mlzDeKMYno|Z`?X|eA@HzQ~!rD)^Wsm1?ba>>x2-3QTkCWNdZ#a0b79gPP znlNR{?-XBUqfU57`P|%N=2P*f^^P;3Z);WVIIApDp z;nX<0=q_EEcLi?>%JScSXXmuGwbf5fZW%j(pUKI|39{O)hmCI<{wL8bwvr*sLme1> z`3HZ*)gO4BT_w&n?%-$^Q0EI@@%7fx>B{D4nrRMO8ZFDWeL}sybqNwpy;s^?5?I?h zXGbWse+o+n)VC;&?Xb3Si;fyCRJe*@mKHX)@QZycxKN9X{v!Pj(xyscF03*htzv-( z{#OpRe)5{{CEkBQqRv64CaPgO7ASQGzb2PL!Y+RlGLw;Z)z`sgO4Hm5`zKXNx53H>#3=!E&E**r-}Y7 zmfd5hREYtL|EuwvUm5=#y_9!dcH`)RyVL$?A}nWCHnoV(zmecD^CsL!V%9)*gH8Q& zjLT@DoV<_kBVV9jQ?>*Yvh^gQKv|obG0ii zmbI9fICiDTJKb5em?vdFQcZqzy3$tm)tSm$MW{wn~K9!L;a(odjKiMf(R^C0c< zcw1ltjVj~VQ~GUr591? zADd&&LaOqGd-EsiEK_~spxoMbSf!b%h7vTah?%JNHY?Hh#E!drg7H;YH5#%P>xk)j z@twO@P6u~?VCKh;-KTZ0N0VkiOfVP4_gJH6%+L?N&DyOMNAR#5pHJOLKK3>)Jj7jot7*QU z4OjL<2d)e>d@03*Q|9r8+MiLyAOSwIO3l1hN8^6>^EC6Y)V)Vj7SzxO9V=kwHg2G2&E}#;aq>#M13V!zg2 zYVpVXmvZ>xf{1ozU_ZBpo0brYHu9^~Fz8WBJb9VQy;k?arKM$RT0@5U8Y{am3C+Zu zvE?h>+Ua`;)z}Dww}(Y?pOixx11@&S$p^=xsjTKUa*FWp`O3~*IJo*YvXQTLbtc`m zE5o(1AlmP&dcBd;K1+Oan;#|_n;~NAcnYg*d0oqQPhX)O0^vyJUynZd@);zbQWSAH z6PU|RbCUVlsq(OV2Jx>8S%z8(`qIOHTkVf-{tBqNDhm2v51)qG; zTi)69dBAh`;PXV*fPS2g)9>iRiHJuFbVNtldw)to?z6%-84Er_=jPYBTF(RWdB9%FMqz?}&E-2GGPW$m5MaGC@v3z4#du?CXCU5~ zrGQ(-&28yJ*_Q^ko-?gxx0DD}q}mPEvFr>?tuoc<_4b&mko#m1yk-{$_J*YY7}};? z`B7db_4-qw8gtz{JKB71YN_*KQURj&j4n!bg;NfJ_QJp_xI6apmU?9vZT$7sTlFPz zySz_sUq0_q@u{k4ZTVh$#++gkf{M6L!^g6bSm}2$!qE9F4a1@4OizvGmmfKg@g8`t zm!&rrCz}j97vR2TG^$h;eY};Gulay*yNy=xgXY+06VF$h`Vp_YUXyo&g(Sm1fkyt;W?M^5%deX92MUzP4l}!i`(vsJ2Nt- z_Y0(zJVnO^xFJQ5c3X0M+i(}}rWU&Ci*xBQ>Esxx1SDQa#P5JG&^k6wb(~U;`T42K zX|}ArI;*DG$WCep37rRouD4Wvy}f!~_by!2OXXEiX*YGKX>+F)FIS$Wz%T~BJ-(JD(Xye?8P}VXj58ac! zg~{iVrtK%HEGs3u*KKHnyv>4u- z=l&$;Z-r8R!JOv<$nui-^&P6yVNX98-)JPi!y45DE_R(PUuPA4U4vS0DhW1XRJ!&C zZC$KI>cCGz@06Gfg%G9v_MVE1CTopT#n(T%Jg7Ie!bgEn>w{u7WrR1Lw@c~+)x|Jn z#X^rteC~9>4gU)mD(P!^?`YyaQ;7DP{MXSsiE}4=G{L>G2DtIKnE1Dz;^G(o^(u*5=`Yg))}fRH{+aw+!gpAR zy^l~~ba|c(lB71)VpUS>cAe*wX4&OGRf!pd8_hQU@W&vZldOzH0V2ov?EiUgxYBPE#*z?*g;755|A*2nLL)RcQs?*%N>QD_)5 z^u~nZL=7M=nVz2h2e6@?H~3i6re@#CZ?!Z^ve4HWjU5opsW_mLR)1=EE3hKMVX2cX zHBVy6OQ!dVsJSjV#9k!yf>SRA^6G3#;@0MB{Q?|)Ek?uJJ@%v*4erHU7DSrsOpG?L5R*R+I;)>lfk#1%Sq{MdIF(8@07*; zNyvN_)^ujrE&Vz3Z02IqW z6`XjRo1uH_o#5&0+W@05*0_3Ta$zz{X+fI6v6;XXMjORT{+o71%M`MJVJS{BYaVC* z_>=gAAA1UvySV}o;XQ6&O(9L}E=sq)UFvkU&7k1Y-=VZ{4Z8h+o!}**V@fzYX!+}g zqK}dae(B*3FAQ$6Q(UW!uM>W$iu+#xb)oIm;JHmh6$E-@^J@=qe&#-r6a9{ZhU(hC zVyT!Y&4tL4D0ZpwC2a!Nl2?u7E*yclix1HI)u&}RoNNC#JIi?(xss@;Yv(zMc#QML zr^z2~(wq#SX4}GU^Y`w~JE=AC5-* zfwSl6Q*DeD`RHNWCI*7)&~rozRrSfYI7f){PVDaQ(GJ(-q_wnr`I#GOrfCd2o9w;L zn?kox(*feWR)9T`t#O!1tpqfVUiXp6V_m*$FB zZMYLv-Nsi%Na#{IAt~AZ8{pQIVBE<)SxZ@NDtTp;Z^Ej5Vzo-Q_>pT40m$M^-0HZuFd z@@XHp5-_E`3N)w*E*HAiHn9KW9}8MO)IW2$x!(g8BC8$p*_MNKM<-8*zeuj->&fTm z54G0EKox}$NjKTFOOhIsNzxzm2TgBrR?tbSO z`u^S;vY9mPf7P7cditj7De0tg9@;1Ag=Mkj;LaI#+x^|Yg#Pel$(DzVFMjWbhQvex zTCJ(l`DfeV6V4UGU>VMrqi@mc`Am&*zBdzv%Y#}pVd&f5yUZ4gmxQ!hBVY;-^~Z!T zvd42JNR{}_&Q*g>LKLge-4WhY8{{J?5UiT}oO#rxdHtcHDAvilFGf=>!iJm7ddpj{ z{Wr*@WB?xaD+HPui;g2RJ`R2}CkJGuM7QrOuf9!1$rG{fT8s|{!Nk&EhEWSgrc3&5 zX^&^?!)267e@GHRGLE_BW*_$6XxS{?@LW}Fa(~Om5taC&#XjDw!s&(NSe_0yi#_SRB+lIc1m@$u09eT<)3C zKeJ+^v$=VT`H@d))U?FP=Cq3X zev{hg@{^@Mn9twR^h6f9bin9oaKRQ5Qrq0NJb?W+Vo4y@8%$Lij2j-N;=q^3_W|`< z-X%3aY|>SZq0?6%}j{U!O)E z@`N5Aot+>%N@%q&DoB(abMSxjyi4eLh8M|M_s>;fA|nruhJ0TU2u)^!f(E}}TwbFwL#iYc!)umD2aZ$jD7WxR4NVYk{`-v}C3$}Q zwz9nQ>r3h0VBn0dM;8g8;NSzC507ubu*1DUK9I$Le9+F9S};%2A3MUn;NRskb`j0q z6J2^~+Gqug7H^TV;M(k6&7;e{QSua+?jlQYyc=f zI^@s|y2XT}GYG2qTwVkYB;@JxSHHqvZ+3lCg?Au7ox#*+?Ze(-ASF%jPEQq?tq30; z(-p}EWM}1Jyz8#;4c(t<-p_P_S`y(jrp-msOhu~zC0fC()JP)e!D@7y>nb=>F9I9zV|w?sNx4VqAmZlJ2Y)o&n8@a2Hw@oR|)dj|h! ziR`Uo+unEya7X~$>cb>TCOclS%dH)3a7jpH%iRp#o8havViVg{+gimD913f-L@36V zBsR!_KqyzKJ}R-+etYdR$m(7oD?t%F9;fAvsr;58K$ySc_(^9RCh(`@;#U0e_~1ii z|88H)f^UvOp7?p+i+8@UilGUScqyLDQs#Sigif!tWCE0fy`=5`#pQhi{zaq)Qe=D1 zdql)xNV-46^_hHZQ|3uUXrgWOOCBFarD+U|qKp;U`;hX4v$-A&^m3=vq!h|bCu21w z=6Wg5@P7f7*guSde=EHSHC`0^8ZO1l!7w@>pnl1@e7duS;pnJm`ykhL>bLu^ql5gq z+H2F#BBnL9@!uqw&whWb`p>t2;x~yLd5pb{dmk@=+u8^gFb=LymA}RatGggL@FCSs zm1P90O`kS7i=cPE)|dohBMOj;FZeL+N_zN<;vXvDD{5J&@N>e8RC4d9e!>$fL0_gj zr;01t*Y=p0i?sI!CRJqHd+T+?oH$;wOV!~}b&KoV}BPx(0Dz`~znqP}a?Z+JKXb>*?s@%GV(w zrx7O3MgJ3#=k4~8Vgq zMSUN2s9j}0#8*~R%K+Kf^O631)shsZ*o-Ez@gJ0Kjz=XPf8?qq*S3Q7Mpss5UPT1nqi-I>3`OB z{J3mXV4J<$mv7=dG{gAtbxlfrFhA!1PPH3HM@K~=B^*X+4`FcFS|;ub|L>-d#C+pu zFp;#58or@E-7)fm>6AauS#w_547Pw9^)1Ut?Uk}ONvD>Nb-RR)+mEFYFdovv>HZ(z_}EyjxdhXsjHM6u-z5KM zTDCIN6%u_an2Bl$Ox0Ebc;o0hHWGg)JJclp`SZjAow6!h`eaE%s0Q~ioK-?2o2m}l zK#{XaDu8mBN!IX(dC{N70k^)Ghth&bCvK~^U`wSyjTzA6$SS?_nJW|wtyMGe_LUXwOlQQc%cdsGu!siE+n zeo_^fpS`_VAfU6hwq8rf)KEfw`t&I@JY4ABXj&Q%;@OvUU+aW288LxB3LFrN$48iB zz$mV93_zCB3dFL7^&A0zn{y}zU&__aIWm(YIDD1Om`Ove)A$`@vSrsggpquVD=&4F z!pMp-Utp><5m0|_i8u%XFybqg^Wn7GYgcQuOFlef;a*^otCl#(MVBNnQ?yL9UjM&;Nfl(Qb)^A+n{k!HC*RdIV`bnDb zU3RIIWBsE`#_Jn0M?=1y#PrJ>gFXl0GVZ=i8PC^q-#I!8;7&mm53{$n?7!j^Tk+iH z6`6PV`ZkutMMqs_~AzN^wdrn(0pO?LBp<+o%&hE_6WY;)lynTs<0NK0WZ>3>_3MS4LP- za}Y?vN^+u>vw*O2B9nOtw&|q6>W?Mf!~?yT0-n8VyOWHi!IpM$i#bT~ILvi6ZNGB0 zXxO;!s6k}RTxwSHAlxMZ8wqt#D`$W`zgm)5 z_Du!dlQ;qw&cWbR_eVX;x!ulsOLQO7@NVeghrcXe7 zyS^RlLRR$hPXdTa?KFCOcg2@bmIE8SeNm@M?F!5vcC>L`l#1qbz7!rSng#O+w--JC zXa4iJd_bz)G}qHL>#f6^_N;B~XU@0YKD_H+H)rA{rZ+sSfRiBFDKsry3_Qc2651df zA7sI3@K$x`p_oNZ(ItbSVCX zfhFpJQ!}0ZtBZg8B6rm#sjup|LJ7saw-7+ z*T!i0{TdbJ4~9iWtDPaA_bTo%(3pD_*h?(8GbmVgXft+k@YOf zhA>+GT$aUY?;nypNv^jpBOvprM+5Vx=eHu>4CY~48+&KNPI{*(?L*bT%3B=drMK}+ z2$cWnH)3_^=)WNoJ{pBBAQ9{!xUd`*XU`|tUDmf4zbLwxH+iZKyNq>Gi~fAR2aOgV zRldZsu|V(P5=K^V;+>(K$0?uk(sfKjLC~(l(?!|@b{YCQJ;#|~GJm@~%!}nhIWZrn z8PKt^;+WNZoOtghFt*g*!Do@3o4HA@?YwbjusG@#N)P9eT?<;6U$_FPT)*b!@-r#IrNAfZV&0=8W~o z=7pr4DTM6i72gpyXQoKWR8mV^A>>uubFbKUIQu~@>VHVs33jRIR&;*pLWcpUM*^hO zcn$~IxkQnVeBc;?#uYLk668+mBNF;*gq|F{X^R8*O|jL=&r+{X3@;(@Z;T7G!i+>9UykZZ`bykho3jI zeY;3H@PfL1hu2ZoiSob}+7%!riJv1%JF$#f7-3VSpT*~uT)-@AW^8rLCcWsbr;t0S zgLpLDdLt|9@P6I>Evrj4f4fl<@;>dmo6bbE`6M@YF@8HqEVE!=$&q_^O>B9*PI|(?2 zgt|IppkD=5mIwShue3BPs!@=Ue1KamGrHOvOU=yu(v*?Uel%T$K|9~bcLbv~0&;>H z9((ovjPk+Wzpc^CK#uV2`SaM%a$2f-{_Yb#a^vn5n5qpU^KTPNZw-FBwC5PAR%5ca zCM#gbMzMx^Blib<0pj`JEkpbLOWaO&KP;qVVKMkvDl02%zSkVHbMUkH;;m855{dq0 zX!hB^Nx>a+=)U*^+v}UT-^GH_id%ly^^I31vudQjZ$D4+_=~{uc9rjB?9Jr%a?Iw< zqfLIIw385j+{;>w&+xr9l9H{eCugjsJ)HVQdQu&0xUH3#`lV_zM{rQ;or{W>nrBH) z78cqu8R!fwd+yf=#uE$mlFivSZCMQ$D z`9kjR@9*HQXhZc+H@GI2D$mKkJYCp<{&HdeU)z%2{>F6GkrN8}q|8?!9FJtSfpbr> zP5;Zd3kI7TEgasJQW`419Fp&sBHT>~^|~aN1xLyp(16P^Si^h}G~pMQJ3bJBhl)6L z&4ERLfU1SvD&K)aISfFZ)Nl(BiS2^t2g<|YWtC?|-yW6TG6y#i2e_{@&04~F`1pFi zxmX?@?K=Q7rDIhFDXL_+Vs863apQ}A$;%?NY;r;ZBPiR;4>mjW${jzHRq~bu zARlErRKNvRgOl&C2N8m+B_1dvG0WcPP`SK_1$+;SQ3q;)Flo?mktKum)>$%BK+ z%gdctKb@%4=N1u(hLgcMe3+MyFOYhq0f2!x)N&0gw1m_ai^yvhp4KxO1M@>0A7j!2WJzL#$Q5i(v>8d(_IM7=911Ry>gUu zzshO>m*0tiN`H^2#0!b$1GH`PS4N*nrVGW<&Zxse03QBWzTBVg8wGyV4JAj#8H6)UC z;kGjc_~2rJF&`BZ17==Axr23l0Ig(|RS1d(9PNaP$IR7;s78Z9+yyWxB6dRs#7iKk zo$mf{0~8d9^X!)g9FT6QsR?bkBA*QEJ_HwASXj7+f1X!6%Da1TFn-X;`*oO5MF`T> zZpbWxA78gUE$#0mw`<;7tDN-Pf4$V3DdN3jC+W7L47Rab%F5>k{cv&1{b_*O@hB{- zacZZLBQ<(vDMBHs$l@bIjt8I{^XSexYm(v1q9PU=3kOB_<}0c`bi2S}Ez{`OmKl z+s~7C4oh#>t9X({M7vutA7?OTKjF7;Uj+V$4A8v{1MtsfzCCtgxuEare|{`@U(Flo zNCn-4_3^<&_5W8_;eQtRn0gq=ES-FIAPWm(ZQ*#F3}qNxG5Q$of7EE3U#;!OMjgqF z^u17Kdc`F20{qu6z#XDJ;ynR(2te4qm{p|+_8-6}P*zdNd1e%RrkZFRYDERR=;gE?9b_CR4!CCvkbgJ236nQ35RgI(@!rWzaK z<2pLT8e=_?aR8hwpm@6rOdB6&K8D_g?z!3mh80inOkk-hwjB(wtGfn-=uz%B(O!w8V9vaS6{aVYpKH`)dAJ$$MG_tDhS!r*-b2B+IFbAaLx?4gaV ztyx$SXCVQFdVn{9%!yS0Bcvf^fvzB6{wOnhiRz$#W7k*njsy`)auNoiIY7x<0L(-& z?-16;UYMrVi~0005Ga8wibR| zu^)+Pi{{$`wPbguI!7Iy!uRjr>A&r~m2mNxeJzd}g5?<&ahL&dXH}C>Ks~9HUu6-s zQ2@FzplH{`xVe!UgMQoNG7*=BPWh+9fL{4A*A_jFj$oI$ZDL|lIM~|Nb(WL!eXEoD zr@-TbEee4ArUU#FcI|J`N(iE+_V`EzJ`YHzE@crfUw#C?;Pbpf^A7r*wee+gYN;o` zuBFQQS`Fs01)ZW;fQQP>%bVUUv3rPaZ$H1|5~93{G~BjHhhdHXbTgvjW+$`>hsEOp zbo*Wt1Ecd)Zc*3y9cDznM(v1q^3*8?i2-=fklkqj+(#4er0d}eFGlFr+&-o@6O2@A z?|WNXf+5Se0og_baIpa^Y;U%f0DxVmWfwsJhElkBGahKKX$wdaxJ1Lo<~#}%V<-ec zm6n!P9+VR+paZLCQ{7YiuxcexQVvylZt+hp1FjiCkHI^W1i0O9c#Gtp&N5lPecEs1 zdg?ifpPwHw#y-yz0fbZDw}mA67ShJ|mgtl!$fN-Aha5=Xz_Xn9^98;@)A#QhV1(4% z(NwUZK+1^vSujN*2!}+0Dj>9BTbYCT`UtF6h)WK#v)yG5lMh9n;UP&zq#-XP8KFCZ zV5#AwM?9{-`%DoprQOgQ$S8@}^qs>l!!3>Z?XOA$;R%$^!LYP$_#b2abp>0FU;Dw% zc66|%UUPipjV+Wr!UgfF#34kGyZJ^k2oj2%U0OL=%zn(??{HfOU@B3)hnGT?$p`Q`yctR`yV)oJs#7wYRaoJ z1exof-~C~HDIOo27OMaX)|n(42C`yp-CBh-ub)uFP-c1nL|(XXIUZhKDh37}Q%o;3 z*nL6Hrl0KsYmEcs$qBZV6|zPCwrBePu#C6Yg?Kz|oJRU1SF&(x1@_xCx3siCpNK3i zL{7!sdj)n`H_QzIZXpPzuUK`X8ilLI=_2*7BO;=9ilg8CIXCa#eRE{VSCV3#^vK-Y z3C0b@EN1s>o;|dW95UDELmQ_ZPx-_6Z@9TDWWR#N5Cxs3uL0cs3Y(w?QOrqhja!?8 z2LuEJp5OSmwbYON8T45MG!P@q{mZKRFKN?{g%r-mL-_=7v=F*4Ob2u9xQQ#w(ZY9x zC})vz$g(yHzx1izY!^VRv1@KHyyi1ap%crWLp;*p?N!T~I$i-*Dex^2Unh)B%cvpE z#R|wE!y07#As;%B&@eM2T{c>g%2HOwzjaXfhj#n^{4v@OETL5!-&uTc)4@mx7P~p2 zA-cQ2-wW}eyfFl4PU?8b#s*8`M(de~@?$(MZml3hLuRuu!UHhgPT(}fZu;#W7e$1H znbig0{aGE2ha~_HsFh4oZh~-WXM}~{Kv*Yi_MH^_4c^o-SR`Q`K=>RTaTgLVn-im8 z2W=82MVync0G@_M->|_68kFXC?_V@1eFf|13!cBy;?l}|6 z2ep8Y5T8iHTwWVg?VW>oEfe-W@S^s9zIO(}&|#Nh;YWDT5CcPyyab^u#N93g2RybB zi6WNNKrVod^cOo9RO_$1f`l*fug}Pykr3gzIQnw*NznH`Yc&xljHENRuz)4x;AkHM z?(CBR5Ls@*HUO8x(f!q21&Ns-S1g@{<}BY}sCASU`5O zrcfG*<=i3d-APg;NGk)vZwQCp$l@43k_ z4|oRz+(*hG`!+?!`)9tf9IyqXkOVP2!tX%-QFV5{ygZoip6Y#kh%NHmvOpd!M8XNc zhd|hQ05&TI@>GL=#^Vg4(%g3M+Q8iFcVFJCfGviKntH|$ZX}+;OXmD}Vh#=t-~?EA zzrX&?Ra2oI!ailupMTwM2gj-$|JDwXG`IQlD)*z~l~p$>c-p#x^a%V9dX?`Y+FwL` z4K@`N00S&{H|LSr_5es6Kt@W2dv;p;ws<11DS&1iHlx4zdStaY03!9B6zN1*KLUYR z6an=FM5k=PC8rr-r?)<-6pQsEkf~-k z2viw6_!uTM0~?IH90fVK#joxUbUh+e{Fbl^WJms|-^AGkz-UkUULd3)bpKp{?K)2N zwXitrP_;n_d>G`m>>wD*%@u^U={lc%VebaQ;EAaTZ_Iqfa-JEiybqk)kapYw>@r@o zHmMxaCvbav&hTs??x9B^jgR2UNxpC`4;S%)pBkE2{Dclsb#y5DKA^E<>CIrHw}9mV z@e4y!hF1V44}`wsv&+jW0MmdI7~naIx_4o8MoQ*+;6d{KJOipq*s*WkU9mGw?d4y1Fg+;OdXvFoW({!Kx8 z?|~O9jc3#p89O7SoNR>WYJBRfuETEk$d!kVYB`*?rCTQwzs$EhZ|6y^r$w1Kco~CpYrm^Z{ED=`0c?P9-_8K)a((9 z-bT09em$9dLYj#gA?~nbV)`rmS%CY=A<3c>2(3gF+&QChmj1tq70f zMPuCl9(~-yPamD0xVxWWBzAoMmDzQ#zy>+(NQNCmmsSvFMs~j!1>7DQC0m= zQbM=Ay{(+tJ2WxzqIap2*}>D3sidUDQSzPcg9lCh{d9Z#a0@qX+|bq6Z)j~jec&e- z9uaX#M&|W3&No@d`%%g~CvHV`4-60%4ZkWW5j`Ckkez)VUiv>fJ0J7&DM(02;I7K6 zs+!i;gqT@aHrB>F`m}87J3C1UPo27FX!!bxB$?h)KQWarHS)+8eo&k^!W}X&ZWDAb zJ;AN6s^e~Y1!Ublo)32UTu?yiSIe=rwH4Ab3&pyG1B!tjTjg5sX0X3&oxKRO@ZQ&c2yb9007>gnry_Vw#!Qc_YA zXXnkFSvp0kX-CGV*HBSB5c}d87#Jin*Y@{ob#`?$#{&n_pNs=2>d0&u>x+bxRQ#``0^lH~z}$H9jk4+3 zDz{hoR-m%p9aJdoW?wpY?CmU8qI*Z4NemqBE?ghu>zS`{KmWDu74k7*N!3;?xyWmj z+B#COLpfbT`D`?$;)b4pEVmca0Xb)6;3K{7&CLXP`T70i&(GzDd<@y#bQXFULfZhp zi6Dw+dTmW@RXAibu-;^I+sH{mZK1DjuTa(gUL-L=mO;0Gp<)vMMD}qz? bl?7ulo9K6sT-~mt;ICVXY6^MsCV~GK`^&0X literal 0 HcmV?d00001 diff --git a/doc/_static/example_7/7_uncertainty_weight.png b/doc/_static/example_7/7_uncertainty_weight.png deleted file mode 100644 index dccd187ec66b82023229899249456653aa9f04d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45769 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"jv(p0xzjoV0fU9o)|7Z(TC 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 diff --git a/doc/_static/example_8/8_magnitude_uncertainty_weight.png b/doc/_static/example_8/8_magnitude_uncertainty_weight.png deleted file mode 100644 index e3d1718ccc81844bf0dcb395094e392d8070a0b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39212 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 literal 0 HcmV?d00001 diff --git a/doc/examples.rst b/doc/examples.rst index a1326a7..c961238 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -204,17 +204,17 @@ by the variation in parameters. Next, the uncertainty residual can be computed for each off-nominal system and uncertainty model. `dkpy` implements six unstructured uncertainty models: -* Additive uncertainty ("A"); +* Additive uncertainty ("A") .. image:: _static/example_7/7_sval_residual_A.png -* Multiplicative input uncertainty ("I"); +* Multiplicative input uncertainty ("I") .. image:: _static/example_7/7_sval_residual_I.png -* Multiplicative output uncertainty ("O"); +* Multiplicative output uncertainty ("O") .. image:: _static/example_7/7_sval_residual_O.png -* Inverse additive uncertainty ("iA"); +* Inverse additive uncertainty ("iA") .. image:: _static/example_7/7_sval_residual_iA.png -* Inverse multiplicative input uncertainty ("iI"); +* Inverse multiplicative input uncertainty ("iI") .. image:: _static/example_7/7_sval_residual_iI.png -* Inverse multiplicative output uncertainty ("iO"). +* Inverse multiplicative output uncertainty ("iO") .. image:: _static/example_7/7_sval_residual_iO.png See Chapter 8.2.3 of [SP06]_ for more information on these unstructured @@ -239,7 +239,10 @@ stable and minimum phase linear time-invariant (LTI) system can be fit to the frequency response of the weights to obtain a LTI description of the uncertainty set. The optimal weight frequency responses and fitted weights are shown below. -.. image:: _static/example_7/7_uncertainty_weight.png +* Left: + .. image:: _static/example_7/7_sval_uncertainty_weight_left.png +* Right: + .. image:: _static/example_7/7_sval_uncertainty_weight_right.png Multi-Model Uncertainty Characterization: Aircraft Actuator Model ----------------------------------------------------------------- @@ -269,7 +272,7 @@ weight is constrained to the identity matrix. Given that the right uncertainty weight is the identity matrix, a fit does not need to be performed for this weight as it will be neglected in the generalized plant. -.. image:: _static/example_8/8_magnitude_uncertainty_weight.png +.. image:: _static/example_8/8_sval_uncertainty_weight_left.png diff --git a/examples/7_uncertainty_characterization.py b/examples/7_uncertainty_characterization.py index eec2817..fd50088 100644 --- a/examples/7_uncertainty_characterization.py +++ b/examples/7_uncertainty_characterization.py @@ -32,15 +32,19 @@ def example_uncertainty_characterization(): ) # Compute uncertainty weight frequency response - response_weight_left, response_weight_right = ( + complex_weight_left, complex_weight_right = ( dkpy.compute_uncertainty_weight_response( response_residuals_dict["inverse_additive"], "diagonal", "diagonal" ) ) # Fit overbounding stable and minimum-phase uncertainty weight system - weight_left = dkpy.fit_uncertainty_weight(response_weight_left, omega, [4, 5]) - weight_right = dkpy.fit_uncertainty_weight(response_weight_right, omega, [3, 5]) + weight_left = dkpy.fit_uncertainty_weight( + complex_weight_left, omega, [4, 5], "left", "diagonal" + ) + weight_right = dkpy.fit_uncertainty_weight( + complex_weight_right, omega, [3, 5], "right", "diagonal" + ) # Plot: Magnitude response of nominal and off-nominal systems dkpy.plot_magnitude_response_uncertain_model_set( @@ -72,15 +76,20 @@ def example_uncertainty_characterization(): response_residuals_dict, omega ) - # Plot: Magnitude response of uncertainty weight frequency response and overbounding - # fit - dkpy.plot_magnitude_response_uncertainty_weight( - response_weight_left, - response_weight_right, + # Plot: Singular value response of left uncertainty weight + dkpy.plot_singular_value_response_uncertainty_weight( + complex_weight_left, omega, weight_left, + ) + + # Plot: Singular value response of right uncertainty weight + dkpy.plot_singular_value_response_uncertainty_weight( + complex_weight_right, + omega, weight_right, ) + plt.show() diff --git a/examples/8_aircraft_actuator_uncertainty_characterization.py b/examples/8_aircraft_actuator_uncertainty_characterization.py index 058b226..bc3b315 100644 --- a/examples/8_aircraft_actuator_uncertainty_characterization.py +++ b/examples/8_aircraft_actuator_uncertainty_characterization.py @@ -26,7 +26,7 @@ def example_aircraft_uncertainty_characterization(): ) # Compute the optimal uncertainty weights with a given structure - response_weight_left, response_weight_right = ( + complex_weight_left, complex_weight_right = ( dkpy.compute_uncertainty_weight_response( response_residual["multiplicative_input"], "scalar", @@ -35,7 +35,9 @@ def example_aircraft_uncertainty_characterization(): ) # Fit an overbounding LTI system to the optimal uncertainty weight response - weight_left = dkpy.fit_uncertainty_weight(response_weight_left, omega, 1) + weight_left = dkpy.fit_uncertainty_weight( + complex_weight_left, omega, 1, "left", "scalar" + ) # Plot: Singular value response of nominal and off-nominal systems dkpy.plot_singular_value_response_uncertain_model_set( @@ -48,15 +50,13 @@ def example_aircraft_uncertainty_characterization(): response_residual, omega, hz=True ) - # Plot: Magnitude response of uncertainty weight frequency response and overbounding - # fit - dkpy.plot_magnitude_response_uncertainty_weight( - response_weight_left, - response_weight_right, + # Plot: Singular value response of left uncertainty weight + dkpy.plot_singular_value_response_uncertainty_weight( + complex_weight_left, omega, - weight_left=weight_left, - hz=True, + weight_left, ) + plt.show() diff --git a/src/dkpy/uncertainty_characterization.py b/src/dkpy/uncertainty_characterization.py index 9447b22..c714b36 100644 --- a/src/dkpy/uncertainty_characterization.py +++ b/src/dkpy/uncertainty_characterization.py @@ -9,7 +9,7 @@ "plot_singular_value_response_uncertain_model_set", "plot_singular_value_response_residual", "plot_singular_value_response_residual_comparison", - "plot_magnitude_response_uncertainty_weight", + "plot_singular_value_response_uncertainty_weight", ] import warnings @@ -1425,7 +1425,7 @@ def plot_singular_value_response_uncertain_model_set( def plot_singular_value_response_residual( - complex_response_residual_dict: Dict[str, np.ndarray], + complex_response_residual_dict: Dict[UncertaintyModelId, np.ndarray], omega: np.ndarray, db: bool = True, hz: bool = False, @@ -1560,7 +1560,7 @@ def plot_singular_value_response_residual( def plot_singular_value_response_residual_comparison( - complex_response_residual_dict: Dict[str, np.ndarray], + complex_response_residual_dict: Dict[UncertaintyModelId, np.ndarray], omega: np.ndarray, db: bool = True, hz: bool = False, @@ -1673,12 +1673,10 @@ def plot_singular_value_response_residual_comparison( return fig, ax, legend -def plot_magnitude_response_uncertainty_weight( - complex_response_weight_left: np.ndarray, - complex_response_weight_right: np.ndarray, +def plot_singular_value_response_uncertainty_weight( + complex_weight: np.ndarray, omega: np.ndarray, - weight_left: Optional[control.StateSpace] = None, - weight_right: Optional[control.StateSpace] = None, + weight_fit: Optional[control.LTI] = None, db: bool = True, hz: bool = False, frequency_log_scale: bool = True, @@ -1686,24 +1684,18 @@ def plot_magnitude_response_uncertainty_weight( plot_response_fit_kw: Dict[str, Any] = {}, subplot_kw: Dict[str, Any] = {}, ) -> Tuple[Figure, Union[Axes, np.ndarray], Legend]: - """Plot the diagonal elements of the optimal left and right uncertainty weight - frequency responses. Optionally, the fitted overbounding left and right uncertainty - weights can also be displayed. + """ + Plot the singular value respone of an uncertainty weight. Parameters ---------- - complex_response_weight_left : np.ndarray, - Frequency response matrices of the left uncertainty weight over a grid of - frequencies. - complex_response_weight_right : np.ndarray, - Frequency response matrices of the right uncertainty weight over a grid of - frequencies. - omega : np.ndarray + complex_weight : Dict[str, np.ndarray] + Dictionary of the uncertainty residual frequency response matrices over a grid + of frequencies for different uncertainty models. + omega : np.narray Angular frequency grid. - weight_left: Optional[control.StateSpace] = None, - State-space model if the fitted overbounding left uncertainty weight. - weight_right: Optional[control.StateSpace] = None, - State-space model if the fitted overbounding right uncertainty weight. + weight_fit : Optional[control.LTI] + Fitted uncertainty weight model. db : bool If True, plot the magnitude in units of dB. Otherwise, plot the magnitude in absolute units. @@ -1713,12 +1705,9 @@ def plot_magnitude_response_uncertainty_weight( frequency_log_scale : bool If True, plot the frequency using a logarithmic axis. Otherwise, plot the the frequency using a linear axis. - plot_response_kw : Dict[str, Any] - Keyword arguments for the frequency response of the uncertainty weight plot. - See [#plot_kw]_ for more information on plotting keywords. - plot_response_fit_kw : Dict[str, Any] - Keyword arguments for the frequency response of the fitted uncertainty weight - plot. See [#plot_kw]_ for more information on plotting keywords. + plot_sval_max_kw : Dict[str, Any] + Keyword arguments for the maximum singular value plot. See [#plot_kw]_ for more + information on plotting keywords. subplot_kw : Dict[str, Any] Keyword arguments for the subplot. See [#subplot_kw]_ for more information on the subplot keywords. @@ -1757,93 +1746,43 @@ def plot_magnitude_response_uncertainty_weight( subplot_kw = {} if subplot_kw is None else subplot_kw subplot_kwargs.update(subplot_kw) - # Uncertainty weight parameters - num_left = complex_response_weight_left.shape[1] - num_right = complex_response_weight_right.shape[1] + # Singular value weight response + sval_weight = np.linalg.svdvals(complex_weight) + sval_weight = control.mag2db(sval_weight) if db else sval_weight - # Magnitude response of the uncertainty weights - magnitude_response_weight_left = np.abs(complex_response_weight_left) - magnitude_response_weight_right = np.abs(complex_response_weight_right) + # Singular value weight fit response + if weight_fit is None: + sval_weight_fit = None + else: + frd_weight_fit = control.FrequencyResponseData(weight_fit, omega, squeeze=False) + response_weight_fit = frd_weight_fit.complex.transpose(2, 0, 1) + sval_weight_fit = np.linalg.svdvals(response_weight_fit) + sval_weight_fit = control.mag2db(sval_weight_fit) if db else sval_weight_fit # Initialize figure - fig, ax = plt.subplots( - max(num_left, num_right), 2, sharex=True, layout="constrained" - ) + fig, ax = plt.subplots(**subplot_kwargs) - # Plot left uncertainty weight frequency response - for idx_left in range(num_left): - ax[idx_left, 0].plot( - omega / (2 * np.pi) if hz else omega, - control.mag2db(magnitude_response_weight_left[:, idx_left, idx_left]) - if db - else magnitude_response_weight_left[:, idx_left, idx_left], - **plot_response_kwargs, - ) - ax[idx_left, 0].set_ylabel( - f"$|W_{{L, ({idx_left + 1}, {idx_left + 1})}}|$ (dB)" - if db - else f"$|W_{{L, ({idx_left + 1}, {idx_left + 1})}}|$ (-)" - ) - ax[idx_left, 0].grid() - # Plot right uncertainty weight frequency response - for idx_right in range(num_left): - ax[idx_right, 1].plot( + # Plot weight singular value response + for idx_sval in range(sval_weight.shape[-1]): + ax.plot( omega / (2 * np.pi) if hz else omega, - control.mag2db(magnitude_response_weight_right[:, idx_right, idx_right]) - if db - else magnitude_response_weight_right[:, idx_right, idx_right], + sval_weight[:, idx_sval], **plot_response_kwargs, ) - ax[idx_right, 1].set_ylabel( - f"$|W_{{R, ({idx_right + 1}, {idx_right + 1})}}|$ (dB)" - if db - else f"$|W_{{R, ({idx_right + 1}, {idx_right + 1})}}|$ (-)" - ) - ax[idx_right, 1].grid() - - # Plot left uncertainty weight fit frequency response - if weight_left is not None: - response_fit_weight_left = control.frequency_response(weight_left, omega) - magnitude_response_fit_weight_left = np.array( - response_fit_weight_left.magnitude - ) - for idx_left in range(num_left): - ax[idx_left, 0].plot( + if sval_weight_fit is not None: + ax.plot( omega / (2 * np.pi) if hz else omega, - control.mag2db( - magnitude_response_fit_weight_left[idx_left, idx_left, :] - ) - if db - else magnitude_response_fit_weight_left[idx_left, idx_left, :], - **plot_response_fit_kwargs, - ) - # Plot right uncertainty weight fit frequency response - if weight_right is not None: - response_fit_weight_right = control.frequency_response(weight_right, omega) - magnitude_response_fit_weight_right = np.array( - response_fit_weight_right.magnitude - ) - for idx_right in range(num_right): - ax[idx_right, 1].plot( - omega / (2 * np.pi) if hz else omega, - control.mag2db( - magnitude_response_fit_weight_right[idx_right, idx_right, :] - ) - if db - else magnitude_response_fit_weight_right[idx_right, idx_right, :], + sval_weight_fit[:, idx_sval], **plot_response_fit_kwargs, ) # Plot settings - for idx_col in range(2): - ax[-1, idx_col].set_xlabel("$f$ (Hz)" if hz else r"$\omega$ (rad/s)") - for ax_row in ax: - for ax_row_col in ax_row: - if frequency_log_scale: - ax_row_col.set_xscale("log") - if not ax_row_col.has_data(): - fig.delaxes(ax_row_col) - handles, labels = ax[0, 0].get_legend_handles_labels() + ax.set_xlabel("$f$ (Hz)" if hz else r"$\omega$ (rad/s)") + ax.set_ylabel("Magnitude (dB)" if db else "Magnitude (-)") + ax.grid() + if frequency_log_scale: + ax.set_xscale("log") + handles, labels = ax.get_legend_handles_labels() legend_dict = dict(zip(labels, handles)) legend = fig.legend( labels=legend_dict.keys(), From 17556c4bded50b1c186d177f18d1bcaac81fd3d6 Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 14 May 2026 13:11:25 -0400 Subject: [PATCH 5/9] Implement uncertainty measure computation. This includes the computation of the equivalent additive uncertainty models from a set of LFT uncertainty models; the computation of an uncertainty model with the minimal size Delta block; and the computation of the uncertainty measure for an additive uncertainty model. --- src/dkpy/uncertainty_characterization.py | 463 ++++++++++++++++++ tests/test_uncertainty_characterization.py | 399 +++++++++++++++ ..._nominal0_offnominal0_omega0_additive_.npz | Bin 0 -> 3058 bytes ..._omega10_inverse_multiplicative_input_.npz | Bin 0 -> 3031 bytes ...omega11_inverse_multiplicative_output_.npz | Bin 0 -> 3035 bytes ...fnominal1_omega1_multiplicative_input_.npz | Bin 0 -> 3056 bytes ...nominal2_omega2_multiplicative_output_.npz | Bin 0 -> 3058 bytes ...3_offnominal3_omega3_inverse_additive_.npz | Bin 0 -> 3056 bytes ...4_omega4_inverse_multiplicative_input_.npz | Bin 0 -> 3058 bytes ..._omega5_inverse_multiplicative_output_.npz | Bin 0 -> 3057 bytes ..._nominal6_offnominal6_omega6_additive_.npz | Bin 0 -> 3021 bytes ...fnominal7_omega7_multiplicative_input_.npz | Bin 0 -> 3018 bytes ...nominal8_omega8_multiplicative_output_.npz | Bin 0 -> 3027 bytes ...9_offnominal9_omega9_inverse_additive_.npz | Bin 0 -> 3034 bytes 14 files changed, 862 insertions(+) create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal0_offnominal0_omega0_additive_.npz create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal10_offnominal10_omega10_inverse_multiplicative_input_.npz create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal11_offnominal11_omega11_inverse_multiplicative_output_.npz create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal1_offnominal1_omega1_multiplicative_input_.npz create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal2_offnominal2_omega2_multiplicative_output_.npz create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal3_offnominal3_omega3_inverse_additive_.npz create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal4_offnominal4_omega4_inverse_multiplicative_input_.npz create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal5_offnominal5_omega5_inverse_multiplicative_output_.npz create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal6_offnominal6_omega6_additive_.npz create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal7_offnominal7_omega7_multiplicative_input_.npz create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal8_offnominal8_omega8_multiplicative_output_.npz create mode 100644 tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal9_offnominal9_omega9_inverse_additive_.npz diff --git a/src/dkpy/uncertainty_characterization.py b/src/dkpy/uncertainty_characterization.py index c714b36..100143c 100644 --- a/src/dkpy/uncertainty_characterization.py +++ b/src/dkpy/uncertainty_characterization.py @@ -3,6 +3,7 @@ __all__ = [ "compute_uncertainty_residual_response", "compute_uncertainty_weight_response", + "compute_uncertainty_measure_response", "fit_uncertainty_weight", "plot_magnitude_response_uncertain_model_set", "plot_phase_response_uncertain_model_set", @@ -955,6 +956,468 @@ def _fit_uncertainty_weight_full( raise NotImplementedError() +def compute_uncertainty_measure_response( + complex_nominal: Union[np.ndarray, control.FrequencyResponseData], + complex_weight_left: Union[np.ndarray, control.FrequencyResponseData], + complex_weight_right: Union[np.ndarray, control.FrequencyResponseData], + uncertainty_model: UncertaintyModelId, +) -> np.ndarray: + """Compute measure (size/volume) frequency response of uncertainty model. + + Parameters + ---------- + complex_nominal : Union[np.ndarray, control.FrequencyResponseData] + Nominal model complex frequency response. + complex_weight_left : Union[np.ndarray, control.FrequencyResponseData] + Left uncertainty weight complex frequency response. + complex_weight_right : Union[np.ndarray, control.FrequencyResponseData] + Right uncertainty weight complex frequency reponse. + uncertainty_model : UncertaintyModelId + Uncertainty model identifier. + + Returns + ------- + np.ndarray + Measure frequency response. + """ + + complex_nominal = _convert_frequency_response_data_to_array(complex_nominal) + complex_weight_left = _convert_frequency_response_data_to_array(complex_weight_left) + complex_weight_right = _convert_frequency_response_data_to_array( + complex_weight_right + ) + + transform_to_additive_dispatcher = { + "additive": _transform_additive_to_additive, + "multiplicative_input": _transform_multiplicative_input_to_additive, + "multiplicative_output": _transform_multiplicative_output_to_additive, + "inverse_additive": _transform_inverse_additive_to_additive, + "inverse_multiplicative_input": _transform_inverse_multiplicative_input_to_additive, + "inverse_multiplicative_output": _transform_inverse_multiplicative_output_to_additive, + } + + # Compute equivalent additive uncertainty model frequency response + complex_uncertainty_add = transform_to_additive_dispatcher[uncertainty_model]( + complex_nominal, complex_weight_left, complex_weight_right + ) + complex_nominal_add = complex_uncertainty_add[0] + complex_weight_left_add = complex_uncertainty_add[1] + complex_weight_right_add = complex_uncertainty_add[2] + + # Compute minimal representation of additive uncertainty frequency response + complex_weight_add_min = _compute_minimal_weights_additive( + complex_weight_left_add, complex_weight_right_add + ) + complex_weight_left_add = complex_weight_add_min[0] + complex_weight_right_add = complex_weight_add_min[1] + + # Compute measure frequency response of minimal additive uncertainty response + measure = _compute_uncertainty_measure_additive( + complex_weight_left_add, complex_weight_right_add + ) + + return measure + + +def _compute_uncertainty_measure_additive( + complex_weight_left: np.ndarray, + complex_weight_right: np.ndarray, +) -> np.ndarray: + """Compute measure (size/volume) frequency response of additive uncertainty. + + Parameters + ---------- + complex_nominal : np.ndarray + Nominal model complex frequency response of additive uncertainty. + complex_weight_left : np.ndarray + Left uncertainty weight complex frequency response of additive uncertainty. + complex_weight_right : np.ndarray + Right uncertainty weight complex frequency response of additive uncertainty + + Returns + ------- + np.ndarray + Measure frequency response of additive uncertainty. + + Raises + ------ + ValueError + Left uncertainty weight is non-square. + ValueError + Right uncertainty weight is non-square. + """ + + # Check uncertainty weight dimensions + if complex_weight_left.shape[1] != complex_weight_left.shape[2]: + raise ValueError( + "Left uncertainty weight must be square (got " + f"{complex_weight_left.shape[1]} rows and {complex_weight_left.shape[2]} " + f"columns)." + ) + if complex_weight_right.shape[1] != complex_weight_right.shape[2]: + raise ValueError( + "Right uncertainty weight must be square (got " + f"{complex_weight_right.shape[1]} rows and {complex_weight_right.shape[2]} " + f"columns)." + ) + + # Auxiliary parameters + nbr_inputs = complex_weight_right.shape[1] + nbr_outputs = complex_weight_left.shape[1] + + # Measure computation + measure_weight_left = np.abs(np.linalg.det(complex_weight_left)) ** ( + 2 / nbr_outputs + ) + measure_weight_right = np.abs(np.linalg.det(complex_weight_right)) ** ( + 2 / nbr_inputs + ) + measure = measure_weight_left * measure_weight_right + + return measure + + +def _transform_additive_to_additive( + complex_nominal: np.ndarray, + complex_weight_left: np.ndarray, + complex_weight_right: np.ndarray, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Transform additive uncertainty frequency response to additive form. + + Parameters + ---------- + complex_nominal : np.ndarray + Nominal model complex frequency response of additive uncertainty. + complex_weight_left : np.ndarray + Left uncertainty weight complex frequency response of additive uncertainty. + complex_weight_right : np.ndarray + Right uncertainty weight complex frequency response of additive uncertainty. + + Returns + ------- + Tuple[np.ndarray, np.ndarray, np.ndarray] + Nominal model, left uncertainty weight, and right uncertainty weight complex + frequency response of equivalent additive uncertainty. + """ + + # Nominal model + complex_nominal_add = complex_nominal + + # Left uncertainty weight + complex_weight_left_add = complex_weight_left + + # Right uncertainty weight + complex_weight_right_add = complex_weight_right + + return complex_nominal_add, complex_weight_left_add, complex_weight_right_add + + +def _transform_multiplicative_input_to_additive( + complex_nominal: np.ndarray, + complex_weight_left: np.ndarray, + complex_weight_right: np.ndarray, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Transform multiplicative input uncertainty frequency response to additive form. + + Parameters + ---------- + complex_nominal : np.ndarray + Nominal model complex frequency response of multiplicative input uncertainty. + complex_weight_left : np.ndarray + Left uncertainty weight complex frequency response of multiplicative input + uncertainty. + complex_weight_right : np.ndarray + Right uncertainty weight complex frequency response of multiplicative input + uncertainty. + + Returns + ------- + Tuple[np.ndarray, np.ndarray, np.ndarray] + Nominal model, left uncertainty weight, and right uncertainty weight complex + frequency response of equivalent additive uncertainty. + """ + + # Nominal model + complexnominal_add = complex_nominal + + # Left uncertainty weight + complex_weight_left_add = complex_nominal @ complex_weight_left + + # Right uncertainty weight + complex_weight_right_add = complex_weight_right + + return complexnominal_add, complex_weight_left_add, complex_weight_right_add + + +def _transform_multiplicative_output_to_additive( + complex_nominal: np.ndarray, + complex_weight_left: np.ndarray, + complex_weight_right: np.ndarray, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Transform multiplicative output uncertainty frequency response to additive form. + + Parameters + ---------- + complex_nominal : np.ndarray + Nominal model complex frequency response of multiplicative output uncertainty. + complex_weight_left : np.ndarray + Left uncertainty weight complex frequency response of multiplicative output + uncertainty. + complex_weight_right : np.ndarray + Right uncertainty weight complex frequency response of multiplicative output + uncertainty. + + Returns + ------- + Tuple[np.ndarray, np.ndarray, np.ndarray] + Nominal model, left uncertainty weight, and right uncertainty weight complex + frequency response of equivalent additive uncertainty. + """ + + # Nominal model + complex_nominal_add = complex_nominal + + # Left uncertainty weight + complex_weight_left_add = complex_weight_left + + # Right uncertainty weight + complex_weight_right_add = complex_weight_right @ complex_nominal + + return complex_nominal_add, complex_weight_left_add, complex_weight_right_add + + +def _transform_inverse_additive_to_additive( + complex_nominal: np.ndarray, + complex_weight_left: np.ndarray, + complex_weight_right: np.ndarray, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Transform inverse additive uncertainty frequency response to additive form. + + Parameters + ---------- + complex_nominal : np.ndarray + Nominal model complex frequency response of inverse additive uncertainty. + complex_weight_left : np.ndarray + Left uncertainty weight complex frequency response of inverse additive + uncertainty. + complex_weight_right : np.ndarray + Right uncertainty weight complex frequency response of inverse additive + uncertainty. + + Returns + ------- + Tuple[np.ndarray, np.ndarray, np.ndarray] + Nominal model, left uncertainty weight, and right uncertainty weight complex + frequency response of equivalent additive uncertainty. + """ + + # Auxiliary variables + product = complex_weight_right @ complex_nominal @ complex_weight_left + product_herm = np.moveaxis(product.conj(), -1, -2) + eye_outer = np.eye(product.shape[1])[None, :, :] + eye_inner = np.eye(product.shape[2])[None, :, :] + + # Nominal model + a_nominal = eye_outer - product @ product_herm + b_nominal = complex_weight_right @ complex_nominal + x_nominal = np.linalg.solve(a_nominal, b_nominal) + complex_nominal_add = ( + complex_nominal + + complex_nominal @ complex_weight_left @ product_herm @ x_nominal + ) + + # Left uncertainty weight + # The transpose using np.moveaxis is required as the linear system solved is in the + # form X @ A = B whereas numpy requires C @ X = D. The original problem is converted + # to the equivalent problem A.T @ X.T = B.T. + a_left = np.moveaxis(scipy.linalg.sqrtm(eye_inner - product_herm @ product), -1, -2) + b_left = np.moveaxis(-(complex_nominal @ complex_weight_left), -1, -2) + complex_weight_left_add = np.moveaxis(np.linalg.solve(a_left, b_left), -1, -2) + + # Right uncertainty weight + a_right = scipy.linalg.sqrtm(eye_outer - product @ product_herm) + b_right = complex_weight_right @ complex_nominal + complex_weight_right_add = np.linalg.solve(a_right, b_right) + + return complex_nominal_add, complex_weight_left_add, complex_weight_right_add + + +def _transform_inverse_multiplicative_input_to_additive( + complex_nominal: np.ndarray, + complex_weight_left: np.ndarray, + complex_weight_right: np.ndarray, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Transform inverse multiplicative input uncertainty frequency response to additive form. + + Parameters + ---------- + complex_nominal : np.ndarray + Nominal model complex frequency response of inverse multiplicative input + uncertainty. + complex_weight_left : np.ndarray + Left uncertainty weight complex frequency response of inverse multiplicative + input uncertainty. + complex_weight_right : np.ndarray + Right uncertainty weight complex frequency response of inverse multiplicative + input uncertainty. + + Returns + ------- + Tuple[np.ndarray, np.ndarray, np.ndarray] + Nominal model, left uncertainty weight, and right uncertainty weight complex + frequency response of equivalent additive uncertainty. + """ + # Auxiliary variables + product = complex_weight_right @ complex_weight_left + product_herm = np.moveaxis(product.conj(), -1, -2) + eye_outer = np.eye(product.shape[1])[None, :, :] + eye_inner = np.eye(product.shape[2])[None, :, :] + + # Nominal model + a_nominal = eye_outer - product @ product_herm + b_nominal = complex_weight_right + x_nominal = np.linalg.solve(a_nominal, b_nominal) + complex_nominal_add = ( + complex_nominal + + complex_nominal @ complex_weight_left @ product_herm @ x_nominal + ) + + # Left uncertainty weight + # The transpose using np.moveaxis is required as the linear system solved is in the + # form X @ A = B whereas numpy requires C @ X = D. The original problem is converted + # to the equivalent problem A.T @ X.T = B.T. + a_left = np.moveaxis(scipy.linalg.sqrtm(eye_inner - product_herm @ product), -1, -2) + b_left = np.moveaxis(-(complex_nominal @ complex_weight_left), -1, -2) + complex_weight_left_add = np.moveaxis(np.linalg.solve(a_left, b_left), -1, -2) + + # Right uncertainty weight + a_right = scipy.linalg.sqrtm(eye_outer - product @ product_herm) + b_right = complex_weight_right + complex_weight_right_add = np.linalg.solve(a_right, b_right) + + return complex_nominal_add, complex_weight_left_add, complex_weight_right_add + + +def _transform_inverse_multiplicative_output_to_additive( + complex_nominal: np.ndarray, + complex_weight_left: np.ndarray, + complex_weight_right: np.ndarray, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Transform inverse multiplicative output uncertainty frequency response to additive form. + + Parameters + ---------- + complex_nominal : np.ndarray + Nominal model complex frequency response of inverse multiplicative output + uncertainty. + complex_weight_left : np.ndarray + Left uncertainty weight complex frequency response of inverse multiplicative + output uncertainty. + complex_weight_right : np.ndarray + Right uncertainty weight complex frequency response of inverse multiplicative + output uncertainty. + + Returns + ------- + Tuple[np.ndarray, np.ndarray, np.ndarray] + Nominal model, left uncertainty weight, and right uncertainty weight complex + frequency response of equivalent additive uncertainty. + """ + # Auxiliary variables + product = complex_weight_right @ complex_weight_left + product_herm = np.moveaxis(product.conj(), -1, -2) + eye_outer = np.eye(product.shape[1])[None, :, :] + eye_inner = np.eye(product.shape[2])[None, :, :] + + # Nominal model + a_nominal = eye_outer - product @ product_herm + b_nominal = complex_weight_right @ complex_nominal + x_nominal = np.linalg.solve(a_nominal, b_nominal) + complex_nominal_add = ( + complex_nominal + complex_weight_left @ product_herm @ x_nominal + ) + + # Left uncertainty weight + # The transpose using np.moveaxis is required as the linear system solved is in the + # form X @ A = B whereas numpy requires C @ X = D. The original problem is converted + # to the equivalent problem A.T @ X.T = B.T. + a_left = np.moveaxis(scipy.linalg.sqrtm(eye_inner - product_herm @ product), -1, -2) + b_left = np.moveaxis(-complex_weight_left, -1, -2) + complex_weight_left_add = np.moveaxis(np.linalg.solve(a_left, b_left), -1, -2) + + # Right uncertainty weight + a_right = scipy.linalg.sqrtm(eye_outer - product @ product_herm) + b_right = complex_weight_right @ complex_nominal + complex_weight_right_add = np.linalg.solve(a_right, b_right) + + return complex_nominal_add, complex_weight_left_add, complex_weight_right_add + + +def _compute_minimal_weights_additive( + complex_weight_left: np.ndarray, + complex_weight_right: np.ndarray, +) -> Tuple[np.ndarray, np.ndarray]: + """Compute minimal dimension additive uncertainty weight frequency response. + + Parameters + ---------- + complex_weight_left : np.ndarray + Left uncertainty weight complex response. + complex_weight_right : np.ndarray + Right uncertainty weight complex response. + + Returns + ------- + Tuple[np.ndarray, np.ndarray] + Minimal left and right uncertainty weight frequency response. + + Raises + ------ + ValueError + The number of perturbation outputs is less than the number of system outputs. + ValueError + The number of perturbation inputs is less than the number of system inputs. + """ + + # Number of inputs/outputs of system and perturbation (Delta block) + nbr_outputs_sys = complex_weight_left.shape[1] + nbr_inputs_sys = complex_weight_right.shape[2] + nbr_outputs_delta = complex_weight_left.shape[2] + nbr_inputs_delta = complex_weight_right.shape[1] + + # Compute minimial left uncertainty weight + if nbr_outputs_sys > nbr_outputs_delta: + raise ValueError( + "The number of perturbation outputs (number of left uncertainty weight " + "columns) must be greater than the number of system outputs (number of " + "left uncertainty weight rows) for additive uncertainty (got " + f"{nbr_outputs_delta} perturbations outputs and {nbr_outputs_sys} system " + "outputs)." + ) + elif nbr_outputs_sys < nbr_outputs_delta: + u_left, s_left, _ = np.linalg.svd(complex_weight_left, full_matrices=False) + complex_weight_left_min = s_left[:, None, :] * u_left # Scale columns of U + else: + complex_weight_left_min = complex_weight_left + + # Compute minimal right uncertainty weight + if nbr_inputs_sys > nbr_inputs_delta: + raise ValueError( + "The number of perturbation inputs (number of right uncertainty weight " + "rows) must be greater than the number of system inputs (number of " + "right uncertainty weight columns) for additive uncertainty (got " + f"{nbr_inputs_delta} perturbations inputs and {nbr_inputs_sys} system " + "inputs)." + ) + elif nbr_inputs_sys < nbr_inputs_delta: + _, s_right, vh_right = np.linalg.svd(complex_weight_right, full_matrices=False) + complex_weight_right_min = s_right[:, :, None] * vh_right # Scale rows of V^H + else: + complex_weight_right_min = complex_weight_right + + return complex_weight_left_min, complex_weight_right_min + + def _convert_frequency_response_data_to_array( frequency_response: Union[np.typing.ArrayLike, control.FrequencyResponseData], ) -> np.ndarray: diff --git a/tests/test_uncertainty_characterization.py b/tests/test_uncertainty_characterization.py index 263e303..621f6ac 100644 --- a/tests/test_uncertainty_characterization.py +++ b/tests/test_uncertainty_characterization.py @@ -929,6 +929,405 @@ def test_convert_frequency_response_data_to_array_error(self, complex_response): ) +class TestComputeUncertaintyMeasureResponse: + @pytest.mark.parametrize( + "nominal, offnominal, omega, uncertainty_model", + [ + ( + control.TransferFunction([1], [0.5, 1]), + [ + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), + ], + np.logspace(-2, 2, 100), + "additive", + ), + ( + control.TransferFunction([1], [0.5, 1]), + [ + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), + ], + np.logspace(-2, 2, 100), + "multiplicative_input", + ), + ( + control.TransferFunction([1], [0.5, 1]), + [ + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), + ], + np.logspace(-2, 2, 100), + "multiplicative_output", + ), + ( + control.TransferFunction([1], [0.5, 1]), + [ + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), + ], + np.logspace(-2, 2, 100), + "inverse_additive", + ), + ( + control.TransferFunction([1], [0.5, 1]), + [ + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), + ], + np.logspace(-2, 2, 100), + "inverse_multiplicative_input", + ), + ( + control.TransferFunction([1], [0.5, 1]), + [ + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), + ], + np.logspace(-2, 2, 100), + "inverse_multiplicative_output", + ), + ( + control.TransferFunction( + [ + [[1], [3]], + [[2], [1]], + ], + [ + [[1, 2 * 0.5 * 1, 1**2], [0.5, 1]], + [[1, 1], [1, 2 * 0.3 * 5, 5**2]], + ], + ), + [ + control.TransferFunction( + [ + [[1], [3.25]], + [[1.9], [1]], + ], + [ + [[1, 2 * 0.4 * 1.1, 1.1**2], [0.6, 1]], + [[0.8, 1], [1, 2 * 0.4 * 4.2, 4.2**2]], + ], + ), + control.TransferFunction( + [ + [[1.1], [2.85]], + [[1.7], [0.9]], + ], + [ + [[1, 2 * 0.45 * 0.9, 0.9**2], [0.55, 1]], + [[0.95, 1], [1, 2 * 0.8 * 5.5, 5.5**2]], + ], + ), + control.TransferFunction( + [ + [[1], [3.05]], + [[1.7], [1.0]], + ], + [ + [[1, 2 * 0.5 * 1.3, 1.3**2], [0.42, 1]], + [[1, 1], [1, 2 * 0.9 * 5.25, 5.25**2]], + ], + ), + ], + np.logspace(-1, 1.5, 100), + "additive", + ), + ( + control.TransferFunction( + [ + [[1], [3]], + [[2], [1]], + ], + [ + [[1, 2 * 0.5 * 1, 1**2], [0.5, 1]], + [[1, 1], [1, 2 * 0.3 * 5, 5**2]], + ], + ), + [ + control.TransferFunction( + [ + [[1], [3.25]], + [[1.9], [1]], + ], + [ + [[1, 2 * 0.4 * 1.1, 1.1**2], [0.6, 1]], + [[0.8, 1], [1, 2 * 0.4 * 4.2, 4.2**2]], + ], + ), + control.TransferFunction( + [ + [[1.1], [2.85]], + [[1.7], [0.9]], + ], + [ + [[1, 2 * 0.45 * 0.9, 0.9**2], [0.55, 1]], + [[0.95, 1], [1, 2 * 0.8 * 5.5, 5.5**2]], + ], + ), + control.TransferFunction( + [ + [[1], [3.05]], + [[1.7], [1.0]], + ], + [ + [[1, 2 * 0.5 * 1.3, 1.3**2], [0.42, 1]], + [[1, 1], [1, 2 * 0.9 * 5.25, 5.25**2]], + ], + ), + ], + np.logspace(-1, 1.5, 100), + "multiplicative_input", + ), + ( + control.TransferFunction( + [ + [[1], [3]], + [[2], [1]], + ], + [ + [[1, 2 * 0.5 * 1, 1**2], [0.5, 1]], + [[1, 1], [1, 2 * 0.3 * 5, 5**2]], + ], + ), + [ + control.TransferFunction( + [ + [[1], [3.25]], + [[1.9], [1]], + ], + [ + [[1, 2 * 0.4 * 1.1, 1.1**2], [0.6, 1]], + [[0.8, 1], [1, 2 * 0.4 * 4.2, 4.2**2]], + ], + ), + control.TransferFunction( + [ + [[1.1], [2.85]], + [[1.7], [0.9]], + ], + [ + [[1, 2 * 0.45 * 0.9, 0.9**2], [0.55, 1]], + [[0.95, 1], [1, 2 * 0.8 * 5.5, 5.5**2]], + ], + ), + control.TransferFunction( + [ + [[1], [3.05]], + [[1.7], [1.0]], + ], + [ + [[1, 2 * 0.5 * 1.3, 1.3**2], [0.42, 1]], + [[1, 1], [1, 2 * 0.9 * 5.25, 5.25**2]], + ], + ), + ], + np.logspace(-1, 1.5, 100), + "multiplicative_output", + ), + ( + control.TransferFunction( + [ + [[1], [3]], + [[2], [1]], + ], + [ + [[1, 2 * 0.5 * 1, 1**2], [0.5, 1]], + [[1, 1], [1, 2 * 0.3 * 5, 5**2]], + ], + ), + [ + control.TransferFunction( + [ + [[1], [3.25]], + [[1.9], [1]], + ], + [ + [[1, 2 * 0.4 * 1.1, 1.1**2], [0.6, 1]], + [[0.8, 1], [1, 2 * 0.4 * 4.2, 4.2**2]], + ], + ), + control.TransferFunction( + [ + [[1.1], [2.85]], + [[1.7], [0.9]], + ], + [ + [[1, 2 * 0.45 * 0.9, 0.9**2], [0.55, 1]], + [[0.95, 1], [1, 2 * 0.8 * 5.5, 5.5**2]], + ], + ), + control.TransferFunction( + [ + [[1], [3.05]], + [[1.7], [1.0]], + ], + [ + [[1, 2 * 0.5 * 1.3, 1.3**2], [0.42, 1]], + [[1, 1], [1, 2 * 0.9 * 5.25, 5.25**2]], + ], + ), + ], + np.logspace(-1, 1.5, 100), + "inverse_additive", + ), + ( + control.TransferFunction( + [ + [[1], [3]], + [[2], [1]], + ], + [ + [[1, 2 * 0.5 * 1, 1**2], [0.5, 1]], + [[1, 1], [1, 2 * 0.3 * 5, 5**2]], + ], + ), + [ + control.TransferFunction( + [ + [[1], [3.25]], + [[1.9], [1]], + ], + [ + [[1, 2 * 0.4 * 1.1, 1.1**2], [0.6, 1]], + [[0.8, 1], [1, 2 * 0.4 * 4.2, 4.2**2]], + ], + ), + control.TransferFunction( + [ + [[1.1], [2.85]], + [[1.7], [0.9]], + ], + [ + [[1, 2 * 0.45 * 0.9, 0.9**2], [0.55, 1]], + [[0.95, 1], [1, 2 * 0.8 * 5.5, 5.5**2]], + ], + ), + control.TransferFunction( + [ + [[1], [3.05]], + [[1.7], [1.0]], + ], + [ + [[1, 2 * 0.5 * 1.3, 1.3**2], [0.42, 1]], + [[1, 1], [1, 2 * 0.9 * 5.25, 5.25**2]], + ], + ), + ], + np.logspace(-1, 1.5, 100), + "inverse_multiplicative_input", + ), + ( + control.TransferFunction( + [ + [[1], [3]], + [[2], [1]], + ], + [ + [[1, 2 * 0.5 * 1, 1**2], [0.5, 1]], + [[1, 1], [1, 2 * 0.3 * 5, 5**2]], + ], + ), + [ + control.TransferFunction( + [ + [[1], [3.25]], + [[1.9], [1]], + ], + [ + [[1, 2 * 0.4 * 1.1, 1.1**2], [0.6, 1]], + [[0.8, 1], [1, 2 * 0.4 * 4.2, 4.2**2]], + ], + ), + control.TransferFunction( + [ + [[1.1], [2.85]], + [[1.7], [0.9]], + ], + [ + [[1, 2 * 0.45 * 0.9, 0.9**2], [0.55, 1]], + [[0.95, 1], [1, 2 * 0.8 * 5.5, 5.5**2]], + ], + ), + control.TransferFunction( + [ + [[1], [3.05]], + [[1.7], [1.0]], + ], + [ + [[1, 2 * 0.5 * 1.3, 1.3**2], [0.42, 1]], + [[1, 1], [1, 2 * 0.9 * 5.25, 5.25**2]], + ], + ), + ], + np.logspace(-1, 1.5, 100), + "inverse_multiplicative_output", + ), + ], + ) + def test_compute_uncertainty_measure_response( + self, + ndarrays_regression, + nominal, + offnominal, + omega, + uncertainty_model, + ): + """Regression test for :func:`compute_uncertainty_measure_response`.""" + + # Frequency response of systems + frd_nominal = control.frequency_response(nominal, omega, squeeze=False) + frd_offnominal_list = control.frequency_response( + offnominal, omega, squeeze=False + ) + + # Uncertainty residual computation + uncertainty_model_list = [uncertainty_model] + complex_residual_dict = dkpy.compute_uncertainty_residual_response( + frd_nominal, + frd_offnominal_list, + uncertainty_model_list, + ) + + measure_dict = {} + for weight_structure in ["scalar", "diagonal", "full"]: + # Uncertainty weight computation + complex_weight_left, complex_weight_right = ( + dkpy.compute_uncertainty_weight_response( + complex_residual_dict[uncertainty_model], + weight_structure, + weight_structure, + ) + ) + + # Uncertainty measure computation + measure = dkpy.compute_uncertainty_measure_response( + frd_nominal, + complex_weight_left, + complex_weight_right, + uncertainty_model, + ) + + measure_dict[weight_structure] = measure + + ndarrays_regression.check( + measure_dict, + default_tolerance=dict(atol=1e-2, rtol=1e-2), + ) + + class TestConvertFrequencyResponseListToArray: @pytest.mark.parametrize( "sys_list, omega", diff --git a/tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal0_offnominal0_omega0_additive_.npz b/tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal0_offnominal0_omega0_additive_.npz new file mode 100644 index 0000000000000000000000000000000000000000..3f1b3bde085ccdb7b52fa143131c6390f3fc8ab2 GIT binary patch literal 3058 zcmeH}c{mj68o*7~7;9?AmSvdi#@dMJ7+DipqU?=^Gj=M*Xc(bl%pg1Cl1XI?X*5Y1 zTcd~=bSgB2DEk&!F3&mLd(VBId(OZ2kKXV3p7;5l?|Z-RuiyLq?5x>2)Y#bAcy^~O zTZCgVdFPMdWfNr!M|uZ%hp7jJU;u1FY(?z5hjt_Ok4$E4IrMR|y;zu`eq?rC5BFbp zY#~BT17uW1*KyDB2K-$6M8fD`*a-}HO^OH+>dat8@&Ym^_cJ2xxIAKgW$;r9R>k{j zxdcqKuF23{wMZ)8K~X!axJ(P;H%`er_Kq1q4}zPR&Ejep=KF=ANC7vGinvn>7dz}Z z#UMM~f(((&#ri5j_8rE^w)zqLbNXvMHvM~(Yq?WygT@CbC}9e{0m&AY!`b9ksQVN| z7SZT)d^z$c98@f7NJluUgrRivnkZX;u_$>=%7ft%&Xq_b*`7o2@rw;@3T4^GscR%g%Qk23^K(2u(h+ z0Jrhh%xmn8g@vg~=^^|L?a|Ijl*9+s&Re0=0X48qY}*>Q6N^nD4RPDWgf`(_}wYM{O-=flF2Wu=JpI0@H{?<61brWg#u7~JRCtsqc zmg1CW9apL1*IDf(TVxt?89AEGpwNP5oG*?ITqxIV8q;|>ES3SnWn2^KFMB~+6_1}W znuy-~MYgJdpHQOp8u{u)JTuT;Od#5+nUL<;ER3d0I}|8?BF?2OBGEOGp{=?Zp4T7! z5?Wq=LE=Py$9jl00#fGiD4h(k$sq^VzRX3_I|5PU$79Ng@1S)f$EfrPn^#?D(+*#v zr(PMLT(@bnWp6)td{N7tD?4d9)i0>q%^!UY9t4et1p8PG%uPQ0B zlLq2SgQAtT?RYkC`wUL%;1!L_%cAw5fRmQlP^PqL`-w4JM2xghL?KXWdO414`$_PF zydccH|LkjuM6b=4`cg}``SVg|-WVx8m$L?UQVfc|O(o7AA2!_Nv-_Xy0(aT<`PJJu zILJHTC%r%E{YmftJH7X;nuLG+yga+~qAmygExDmQt>b6=#06Q1J_E@fFYOKlbB?=J zGfk6=0>HyU3_Be1gW@$YisVwbi@2b6>7tBGzU0)oF_tnn1^P9P&TE^+JE$Zc5CR{{ z-QsJE!8SQ=Hol27^*wUGPq-bBCXeXkDb}X?a%tyWGD8)AOLcKcJ+Ejav(XKS$B4Y~ z&-URaUj2I8QE(+wbO7ggB&z8&JHmhC_QF-wMQ{D4_%_PY9rM4>DP3#5Yop9 z?bUw-KN8BD(s(euKgQJbGlI~~j5#3olDc5F#$75MxI^hZDkRbV!f}*BghI zGC$6AX2p3eo-@3Z9c%&WAC1=x3w`Op&)rW|*-+zWZq>1DT|fZo2y;)=0peHbQ;`qu zbH=fBeUB165-ppp;ClVUh^|cahK8ylCi>hJH9J0iea4#AV*wKP>=WCOL6*}Qea&?@ z?rq6z-{U0+Uu;<6YV9Gt;bPsax2NxwE&E!eNac*fovQ)^5uaOny1Wlo!pe~T>LK~E z4sZnJ1^alL7DazWZ_koVRHp!m^v~&KRE5T6Ghi=1uvOn*&ShLJ!9w~CHjRk zMq8m1{*GBLQR9^_im8tY8jbhjUtM|D+vS*;5Zu)Sj`S?D-L?(Hl7M*aZqYLiB9{b@ z_8-0K9A7*4AnNi?w2=L3<$Db7Tv1s2Fs0qctK2PpqxCrd@Rw0;!x$Tr*bQNQ-)^Jj zis>)W(OLq6s4|c$)JQUB;Pj+hk}2-}WR1ZExMFzJwD@H;;K}?~z9Zcucr*SP6^&4s zV?i-Vep7}6Hu&AJ#+sNv4ALUwtw{wJ8Mp^y&d-5ac?*4@a?Vs E36eyJRsaA1 literal 0 HcmV?d00001 diff --git a/tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal10_offnominal10_omega10_inverse_multiplicative_input_.npz b/tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal10_offnominal10_omega10_inverse_multiplicative_input_.npz new file mode 100644 index 0000000000000000000000000000000000000000..6a313556376ea7840305d58dd48b0a1a18d94063 GIT binary patch literal 3031 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 QxW5D58)bW0oo9dbJ3_RgB>(^b literal 0 HcmV?d00001 diff --git a/tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal2_offnominal2_omega2_multiplicative_output_.npz b/tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal2_offnominal2_omega2_multiplicative_output_.npz new file mode 100644 index 0000000000000000000000000000000000000000..91e59f34dbbe0882e43af5b9668d21c35482e485 GIT binary patch literal 3058 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 N+Z?3LtSh>;`U3+6o>Kq- literal 0 HcmV?d00001 diff --git a/tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal3_offnominal3_omega3_inverse_additive_.npz b/tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal3_offnominal3_omega3_inverse_additive_.npz new file mode 100644 index 0000000000000000000000000000000000000000..80fdef45fac7f35b7c6924cd5c8411050ca228cd GIT binary patch literal 3056 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 literal 0 HcmV?d00001 diff --git a/tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal9_offnominal9_omega9_inverse_additive_.npz b/tests/test_uncertainty_characterization/test_compute_uncertainty_measure_response_nominal9_offnominal9_omega9_inverse_additive_.npz new file mode 100644 index 0000000000000000000000000000000000000000..a1ce2a4f206c87dfcd85426aa8638f2ef402d919 GIT binary patch literal 3034 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 q`X9V~v9V literal 0 HcmV?d00001 From 3e39e517fdf370e9ac978564e824cd4579d51e49 Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 14 May 2026 13:13:47 -0400 Subject: [PATCH 6/9] Implement uncertainty measure plotting function. --- src/dkpy/uncertainty_characterization.py | 86 +++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/src/dkpy/uncertainty_characterization.py b/src/dkpy/uncertainty_characterization.py index 100143c..4917f13 100644 --- a/src/dkpy/uncertainty_characterization.py +++ b/src/dkpy/uncertainty_characterization.py @@ -2147,8 +2147,7 @@ def plot_singular_value_response_uncertainty_weight( plot_response_fit_kw: Dict[str, Any] = {}, subplot_kw: Dict[str, Any] = {}, ) -> Tuple[Figure, Union[Axes, np.ndarray], Legend]: - """ - Plot the singular value respone of an uncertainty weight. + """Plot the singular value respone of an uncertainty weight. Parameters ---------- @@ -2255,3 +2254,86 @@ def plot_singular_value_response_uncertainty_weight( ) return fig, ax, legend + + +def plot_uncertainty_measure( + measure: np.ndarray, + omega: np.ndarray, + db: bool = True, + hz: bool = False, + frequency_log_scale: bool = True, + plot_kw: Dict[str, Any] = {}, + subplot_kw: Dict[str, Any] = {}, +) -> Tuple[Figure, Union[Axes, np.ndarray]]: + """Plot uncertainty measure frequency response. + + Parameters + ---------- + measure : np.ndarray + Uncertainty measure frequency response. + omega : np.narray + Angular frequency grid. + weight_fit : Optional[control.LTI] + Fitted uncertainty weight model. + db : bool + If True, plot the magnitude in units of dB. Otherwise, plot the magnitude in + absolute units. + hz : bool + If True, plot the frequency in units of Hz. Otherwise, plot the frequency in + units of rad/s. + frequency_log_scale : bool + If True, plot the frequency using a logarithmic axis. Otherwise, plot the + the frequency using a linear axis. + plot_sval_max_kw : Dict[str, Any] + Keyword arguments for the maximum singular value plot. See [#plot_kw]_ for more + information on plotting keywords. + subplot_kw : Dict[str, Any] + Keyword arguments for the subplot. See [#subplot_kw]_ for more information on + the subplot keywords. + + Returns + ------- + Tuple[Figure, Union[Axes, np.ndarray], Legend] + Matplotlib Figure object, Axes object (or np.ndarray of Axes objects), and + Legend object. + + References + ---------- + .. [#plot_kw] https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html + .. [#subplot_kw] https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.subplots.html + + """ + # Plot keyword arguments + plot_kwargs = { + "color": "C0", + "marker": "", + "linestyle": "-", + } + plot_kwargs.update(plot_kw) + + # Subplot keyword arguments + subplot_kwargs = {"sharex": True, "layout": "constrained"} + subplot_kw = {} if subplot_kw is None else subplot_kw + subplot_kwargs.update(subplot_kw) + + # Measure response + measure = control.mag2db(measure) if db else measure + + # Initialize figure + fig, ax = plt.subplots(**subplot_kwargs) + + # Plot weight singular value response + ax.plot( + omega / (2 * np.pi) if hz else omega, + measure, + **plot_kwargs, + ) + + # Plot settings + ax.set_xlabel("$f$ (Hz)" if hz else r"$\omega$ (rad/s)") + ax.set_ylabel("Magnitude (dB)" if db else "Magnitude (-)") + ax.grid() + if frequency_log_scale: + ax.set_xscale("log") + + return fig, ax From 21ed46c282303935fda151b80529ef1fd5715c07 Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 14 May 2026 13:31:12 -0400 Subject: [PATCH 7/9] Add docstring example for uncertainty measure computation. --- examples/7_uncertainty_characterization.py | 7 +++ src/dkpy/uncertainty_characterization.py | 50 ++++++++++++++++------ test.py | 0 3 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 test.py diff --git a/examples/7_uncertainty_characterization.py b/examples/7_uncertainty_characterization.py index fd50088..6e35a44 100644 --- a/examples/7_uncertainty_characterization.py +++ b/examples/7_uncertainty_characterization.py @@ -46,6 +46,13 @@ def example_uncertainty_characterization(): complex_weight_right, omega, [3, 5], "right", "diagonal" ) + measure = dkpy.compute_uncertainty_measure_response( + complex_nominal, + complex_weight_left, + complex_weight_right, + "multiplicative_input", + ) + # Plot: Magnitude response of nominal and off-nominal systems dkpy.plot_magnitude_response_uncertain_model_set( response_nom, diff --git a/src/dkpy/uncertainty_characterization.py b/src/dkpy/uncertainty_characterization.py index 4917f13..5a98e36 100644 --- a/src/dkpy/uncertainty_characterization.py +++ b/src/dkpy/uncertainty_characterization.py @@ -467,7 +467,7 @@ def compute_uncertainty_weight_response( residual frequency response where the left and right uncertainty weights are assumed to be diagonal. - >>> complex_response_nom, complex_response_offnom_list, omega = ( + >>> complex_nominal, complex_offnominal_list, omega = ( ... example_multimodel_uncertainty ... ) >>> uncertainty_models = [ @@ -478,14 +478,14 @@ def compute_uncertainty_weight_response( ... "inverse_multiplicative_input", ... "inverse_multiplicative_output", ... ] - >>> complex_response_residual_dict = compute_uncertainty_residual_response( - ... complex_response_nom, - ... complex_response_offnom_list, + >>> complex_residual_dict = compute_uncertainty_residual_response( + ... complex_nominal, + ... complex_offnominal_list, ... uncertainty_models, ... ) - >>> complex_response_weight_left, complex_response_weight_right = ( + >>> complex_weight_left, complex_weight_right = ( ... dkpy.compute_uncertainty_weight_response( - ... complex_response_residual_dict["multiplicative_input"], + ... complex_residual_dict["multiplicative_input"], ... "diagonal", ... "diagonal", ... ) @@ -666,14 +666,7 @@ def fit_uncertainty_weight( >>> complex_response_nom, complex_response_offnom_list, omega = ( ... example_multimodel_uncertainty ... ) - >>> uncertainty_models = { - ... "additive", - ... "multiplicative_input", - ... "multiplicative_output", - ... "inverse_additive", - ... "inverse_multiplicative_input", - ... "inverse_multiplicative_output", - ... } + >>> uncertainty_models = ["multiplicative_input"] >>> complex_response_residual_dict = compute_uncertainty_residual_response( ... complex_response_nom, ... complex_response_offnom_list, @@ -979,6 +972,35 @@ def compute_uncertainty_measure_response( ------- np.ndarray Measure frequency response. + + + Examples + -------- + Compute the uncertainty measure response for a multiplicative input uncertainty + model. + + >>> complex_nominal, complex_offnominal_list, omega = ( + ... example_multimodel_uncertainty + ... ) + >>> uncertainty_models = ["multiplicative_input"] + >>> complex_residual_dict = compute_uncertainty_residual_response( + ... complex_nominal, + ... complex_offnominal_list, + ... uncertainty_models, + ... ) + >>> complex_weight_left, complex_weight_right = ( + ... dkpy.compute_uncertainty_weight_response( + ... complex_residual_dict["multiplicative_input"], + ... "diagonal", + ... "diagonal", + ... ) + ... ) + >>> measure = dkpy.compute_uncertainty_measure_response( + ... complex_nominal, + ... complex_weight_left, + ... complex_weight_right, + ... "multiplicative_input", + ... ) """ complex_nominal = _convert_frequency_response_data_to_array(complex_nominal) diff --git a/test.py b/test.py new file mode 100644 index 0000000..e69de29 From 0f53cf24790d58f9010f74a9882305272880d366 Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 22 May 2026 11:51:55 -0400 Subject: [PATCH 8/9] Remove UncertaintyModelId type hint definition in favor of string type hints. --- src/dkpy/uncertainty_characterization.py | 56 +++++++++++++----------- test.py | 0 2 files changed, 31 insertions(+), 25 deletions(-) delete mode 100644 test.py diff --git a/src/dkpy/uncertainty_characterization.py b/src/dkpy/uncertainty_characterization.py index 5a98e36..48cc8bb 100644 --- a/src/dkpy/uncertainty_characterization.py +++ b/src/dkpy/uncertainty_characterization.py @@ -11,9 +11,9 @@ "plot_singular_value_response_residual", "plot_singular_value_response_residual_comparison", "plot_singular_value_response_uncertainty_weight", + "plot_uncertainty_measure", ] -import warnings import control import numpy as np @@ -28,22 +28,13 @@ from . import lti_system_fit -UncertaintyModelId = Literal[ - "additive", - "multiplicative_input", - "multiplicative_output", - "inverse_additive", - "inverse_multiplicative_input", - "inverse_multiplicative_output", -] - def compute_uncertainty_residual_response( complex_response_nom: Union[np.ndarray, control.FrequencyResponseData], complex_response_offnom_list: Union[np.ndarray, control.FrequencyResponseList], - uncertainty_model: Union[UncertaintyModelId, List[UncertaintyModelId]], + uncertainty_model: Union[str, List[str]], tol_residual_existence: float = 1e-12, -) -> Dict[UncertaintyModelId, np.ndarray]: +) -> Dict[str, np.ndarray]: """Compute the residual response of unstructured uncertainty models. Parameters @@ -52,8 +43,15 @@ def compute_uncertainty_residual_response( Frequency response of the nominal system. complex_response_offnom_list : Union[np.ndarray, control.FrequencyResponseList] Frequency response of the off-nominal system. - uncertainty_model : Union[UncertaintyModelId, List[UncertaintyModelId]] - Uncertainty model identifiers to compute the residual response. + uncertainty_model : Union[str, List[str]] + Uncertainty model identifiers to compute the residual response. The valid + uncertainty model identifiers are: + - "additive" + - "multiplicative_input" + - "multiplicative_output" + - "inverse_additive" + - "inverse_multiplicative_input", + - "inverse_multiplicative_output" tol_residual_existence : float Tolerance for the existence of an uncertainty residual. @@ -75,14 +73,14 @@ def compute_uncertainty_residual_response( >>> complex_response_nom, complex_response_offnom_list, omega = ( ... example_multimodel_uncertainty ... ) - >>> uncertainty_models = { + >>> uncertainty_models = [ ... "additive", ... "multiplicative_input", ... "multiplicative_output", ... "inverse_additive", ... "inverse_multiplicative_input", ... "inverse_multiplicative_output", - ... } + ... ] >>> complex_response_residuals_dict = dkpy.compute_uncertainty_residual_response( ... complex_response_nom, ... complex_response_offnom_list, @@ -115,10 +113,10 @@ def compute_uncertainty_residual_response( compute_residual_model = compute_residual_dispatcher[model] except KeyError: raise KeyError( - "The uncertainty model identifier must be `additive`, " - "`multiplicative_input`, `multiplicative_output`, `inverse_additive` " - "`inverse_multiplicative_input`, or `inverse_multiplicative_output` " - f"(got `{model}`)." + 'The uncertainty model identifier must be "additive", ' + '"multiplicative_input", "multiplicative_output", "inverse_additive" ' + '"inverse_multiplicative_input", or "inverse_multiplicative_output" ' + f'(got "{model}").' ) complex_response_residual_list = compute_residual_model( complex_response_nom, @@ -953,7 +951,7 @@ def compute_uncertainty_measure_response( complex_nominal: Union[np.ndarray, control.FrequencyResponseData], complex_weight_left: Union[np.ndarray, control.FrequencyResponseData], complex_weight_right: Union[np.ndarray, control.FrequencyResponseData], - uncertainty_model: UncertaintyModelId, + uncertainty_model: str, ) -> np.ndarray: """Compute measure (size/volume) frequency response of uncertainty model. @@ -965,8 +963,16 @@ def compute_uncertainty_measure_response( Left uncertainty weight complex frequency response. complex_weight_right : Union[np.ndarray, control.FrequencyResponseData] Right uncertainty weight complex frequency reponse. - uncertainty_model : UncertaintyModelId - Uncertainty model identifier. + uncertainty_model : str + Uncertainty model identifier to compute the residual response. The valid + uncertainty model identifiers are: + - "additive" + - "multiplicative_input" + - "multiplicative_output" + - "inverse_additive" + - "inverse_multiplicative_input", + - "inverse_multiplicative_output" + Returns ------- @@ -1910,7 +1916,7 @@ def plot_singular_value_response_uncertain_model_set( def plot_singular_value_response_residual( - complex_response_residual_dict: Dict[UncertaintyModelId, np.ndarray], + complex_response_residual_dict: Dict[str, np.ndarray], omega: np.ndarray, db: bool = True, hz: bool = False, @@ -2045,7 +2051,7 @@ def plot_singular_value_response_residual( def plot_singular_value_response_residual_comparison( - complex_response_residual_dict: Dict[UncertaintyModelId, np.ndarray], + complex_response_residual_dict: Dict[str, np.ndarray], omega: np.ndarray, db: bool = True, hz: bool = False, diff --git a/test.py b/test.py deleted file mode 100644 index e69de29..0000000 From 6e15e21821717b6d98ca0b7e23efb8df8a3d3674 Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 22 May 2026 13:32:59 -0400 Subject: [PATCH 9/9] Implement unit tests for the transformation of an uncertainty model to its additive equivalent model. --- src/dkpy/uncertainty_characterization.py | 12 +- tests/test_uncertainty_characterization.py | 803 +++++++++++++-------- 2 files changed, 520 insertions(+), 295 deletions(-) diff --git a/src/dkpy/uncertainty_characterization.py b/src/dkpy/uncertainty_characterization.py index 48cc8bb..9b6a7e0 100644 --- a/src/dkpy/uncertainty_characterization.py +++ b/src/dkpy/uncertainty_characterization.py @@ -1242,8 +1242,8 @@ def _transform_inverse_additive_to_additive( # Auxiliary variables product = complex_weight_right @ complex_nominal @ complex_weight_left product_herm = np.moveaxis(product.conj(), -1, -2) - eye_outer = np.eye(product.shape[1])[None, :, :] - eye_inner = np.eye(product.shape[2])[None, :, :] + eye_outer = np.eye(product.shape[-2])[None, :, :] + eye_inner = np.eye(product.shape[-1])[None, :, :] # Nominal model a_nominal = eye_outer - product @ product_herm @@ -1298,8 +1298,8 @@ def _transform_inverse_multiplicative_input_to_additive( # Auxiliary variables product = complex_weight_right @ complex_weight_left product_herm = np.moveaxis(product.conj(), -1, -2) - eye_outer = np.eye(product.shape[1])[None, :, :] - eye_inner = np.eye(product.shape[2])[None, :, :] + eye_outer = np.eye(product.shape[-2])[None, :, :] + eye_inner = np.eye(product.shape[-1])[None, :, :] # Nominal model a_nominal = eye_outer - product @ product_herm @@ -1354,8 +1354,8 @@ def _transform_inverse_multiplicative_output_to_additive( # Auxiliary variables product = complex_weight_right @ complex_weight_left product_herm = np.moveaxis(product.conj(), -1, -2) - eye_outer = np.eye(product.shape[1])[None, :, :] - eye_inner = np.eye(product.shape[2])[None, :, :] + eye_outer = np.eye(product.shape[-2])[None, :, :] + eye_inner = np.eye(product.shape[-1])[None, :, :] # Nominal model a_nominal = eye_outer - product @ product_herm diff --git a/tests/test_uncertainty_characterization.py b/tests/test_uncertainty_characterization.py index 621f6ac..a2f0e25 100644 --- a/tests/test_uncertainty_characterization.py +++ b/tests/test_uncertainty_characterization.py @@ -640,11 +640,234 @@ def test_compute_uncertainty_weight_response( ) -class TestFitUncertaintyWeight: - """Test :func:`fit_uncertainty_weight`.""" +class TestTransformUncertaintyModelToAdditive: + @pytest.mark.parametrize( + "complex_nominal, complex_weight_left, complex_weight_right, " + "complex_nominal_add_exp, complex_weight_left_add_exp, " + "complex_weight_right_add_exp", + [(3 * np.eye(3), np.eye(3), np.eye(3), 3 * np.eye(3), np.eye(3), np.eye(3))], + ) + def test_transform_additive_to_additive( + self, + complex_nominal, + complex_weight_left, + complex_weight_right, + complex_nominal_add_exp, + complex_weight_left_add_exp, + complex_weight_right_add_exp, + ): + from dkpy.uncertainty_characterization import _transform_additive_to_additive + + complex_additive = _transform_additive_to_additive( + complex_nominal, complex_weight_left, complex_weight_right + ) + complex_nominal_add = complex_additive[0] + complex_weight_left_add = complex_additive[1] + complex_weight_right_add = complex_additive[2] + + assert np.allclose(complex_nominal_add, complex_nominal_add_exp) + assert np.allclose(complex_weight_left_add, complex_weight_left_add_exp) + assert np.allclose(complex_weight_right_add, complex_weight_right_add_exp) @pytest.mark.parametrize( - "sys_nom, sys_offnom_list, omega, weight_left_structure, weight_right_structure, fit_order", + "complex_nominal, complex_weight_left, complex_weight_right, " + "complex_nominal_add_exp, complex_weight_left_add_exp, " + "complex_weight_right_add_exp", + [ + ( + 3 * np.eye(3), + np.eye(3), + np.eye(3), + 3 * np.eye(3), + 3 * np.eye(3), + np.eye(3), + ) + ], + ) + def test_transform_multiplicative_input_to_additive( + self, + complex_nominal, + complex_weight_left, + complex_weight_right, + complex_nominal_add_exp, + complex_weight_left_add_exp, + complex_weight_right_add_exp, + ): + from dkpy.uncertainty_characterization import ( + _transform_multiplicative_input_to_additive, + ) + + complex_additive = _transform_multiplicative_input_to_additive( + complex_nominal, complex_weight_left, complex_weight_right + ) + complex_nominal_add = complex_additive[0] + complex_weight_left_add = complex_additive[1] + complex_weight_right_add = complex_additive[2] + + assert np.allclose(complex_nominal_add, complex_nominal_add_exp) + assert np.allclose(complex_weight_left_add, complex_weight_left_add_exp) + assert np.allclose(complex_weight_right_add, complex_weight_right_add_exp) + + @pytest.mark.parametrize( + "complex_nominal, complex_weight_left, complex_weight_right, " + "complex_nominal_add_exp, complex_weight_left_add_exp, " + "complex_weight_right_add_exp", + [ + ( + 3 * np.eye(3), + np.eye(3), + np.eye(3), + 3 * np.eye(3), + np.eye(3), + 3 * np.eye(3), + ) + ], + ) + def test_transform_multiplicative_output_to_additive( + self, + complex_nominal, + complex_weight_left, + complex_weight_right, + complex_nominal_add_exp, + complex_weight_left_add_exp, + complex_weight_right_add_exp, + ): + from dkpy.uncertainty_characterization import ( + _transform_multiplicative_output_to_additive, + ) + + complex_additive = _transform_multiplicative_output_to_additive( + complex_nominal, complex_weight_left, complex_weight_right + ) + complex_nominal_add = complex_additive[0] + complex_weight_left_add = complex_additive[1] + complex_weight_right_add = complex_additive[2] + + assert np.allclose(complex_nominal_add, complex_nominal_add_exp) + assert np.allclose(complex_weight_left_add, complex_weight_left_add_exp) + assert np.allclose(complex_weight_right_add, complex_weight_right_add_exp) + + @pytest.mark.parametrize( + "complex_nominal, complex_weight_left, complex_weight_right, " + "complex_nominal_add_exp, complex_weight_left_add_exp, " + "complex_weight_right_add_exp", + [ + ( + 3 * np.eye(3), + 0.5 * np.eye(3), + 0.5 * np.eye(3), + 6.85714 * np.eye(3), + -2.26779 * np.eye(3), + 2.26779 * np.eye(3), + ) + ], + ) + def test_transform_inverse_additive_to_additive( + self, + complex_nominal, + complex_weight_left, + complex_weight_right, + complex_nominal_add_exp, + complex_weight_left_add_exp, + complex_weight_right_add_exp, + ): + from dkpy.uncertainty_characterization import ( + _transform_inverse_additive_to_additive, + ) + + complex_additive = _transform_inverse_additive_to_additive( + complex_nominal, complex_weight_left, complex_weight_right + ) + complex_nominal_add = complex_additive[0] + complex_weight_left_add = complex_additive[1] + complex_weight_right_add = complex_additive[2] + + assert np.allclose(complex_nominal_add, complex_nominal_add_exp) + assert np.allclose(complex_weight_left_add, complex_weight_left_add_exp) + assert np.allclose(complex_weight_right_add, complex_weight_right_add_exp) + + @pytest.mark.parametrize( + "complex_nominal, complex_weight_left, complex_weight_right, " + "complex_nominal_add_exp, complex_weight_left_add_exp, " + "complex_weight_right_add_exp", + [ + ( + 3 * np.eye(3), + 0.5 * np.eye(3), + 0.5 * np.eye(3), + 3.2 * np.eye(3), + -1.54919 * np.eye(3), + 0.51640 * np.eye(3), + ) + ], + ) + def test_transform_inverse_multiplicative_input_to_additive( + self, + complex_nominal, + complex_weight_left, + complex_weight_right, + complex_nominal_add_exp, + complex_weight_left_add_exp, + complex_weight_right_add_exp, + ): + from dkpy.uncertainty_characterization import ( + _transform_inverse_multiplicative_input_to_additive, + ) + + complex_additive = _transform_inverse_multiplicative_input_to_additive( + complex_nominal, complex_weight_left, complex_weight_right + ) + complex_nominal_add = complex_additive[0] + complex_weight_left_add = complex_additive[1] + complex_weight_right_add = complex_additive[2] + + assert np.allclose(complex_nominal_add, complex_nominal_add_exp) + assert np.allclose(complex_weight_left_add, complex_weight_left_add_exp) + assert np.allclose(complex_weight_right_add, complex_weight_right_add_exp) + + @pytest.mark.parametrize( + "complex_nominal, complex_weight_left, complex_weight_right, " + "complex_nominal_add_exp, complex_weight_left_add_exp, " + "complex_weight_right_add_exp", + [ + ( + 3 * np.eye(3), + 0.5 * np.eye(3), + 0.5 * np.eye(3), + 3.2 * np.eye(3), + -0.51640 * np.eye(3), + 1.54919 * np.eye(3), + ) + ], + ) + def test_transform_inverse_multiplicative_output_to_additive( + self, + complex_nominal, + complex_weight_left, + complex_weight_right, + complex_nominal_add_exp, + complex_weight_left_add_exp, + complex_weight_right_add_exp, + ): + from dkpy.uncertainty_characterization import ( + _transform_inverse_multiplicative_output_to_additive, + ) + + complex_additive = _transform_inverse_multiplicative_output_to_additive( + complex_nominal, complex_weight_left, complex_weight_right + ) + complex_nominal_add = complex_additive[0] + complex_weight_left_add = complex_additive[1] + complex_weight_right_add = complex_additive[2] + + assert np.allclose(complex_nominal_add, complex_nominal_add_exp) + assert np.allclose(complex_weight_left_add, complex_weight_left_add_exp) + assert np.allclose(complex_weight_right_add, complex_weight_right_add_exp) + + +class TestComputeUncertaintyMeasureResponse: + @pytest.mark.parametrize( + "nominal, offnominal, omega, uncertainty_model", [ ( control.TransferFunction([1], [0.5, 1]), @@ -655,22 +878,62 @@ class TestFitUncertaintyWeight: control.TransferFunction([1], [0.7, 1]), ], np.logspace(-2, 2, 100), - "diagonal", - "diagonal", - 4, + "additive", ), ( - control.TransferFunction([1], [1, 2 * 0.5 * 1, 1**2]), + control.TransferFunction([1], [0.5, 1]), [ - control.TransferFunction([1], [1, 2 * 0.3 * 1.5, 1.5**2]), - control.TransferFunction([1], [1, 2 * 0.7 * 1.3, 1.3**2]), - control.TransferFunction([1], [1, 2 * 0.2 * 0.9, 0.9**2]), - control.TransferFunction([1], [1, 2 * 0.9 * 0.7, 0.7**2]), + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), ], - np.logspace(-1, 1, 100), - "diagonal", - "diagonal", - 4, + np.logspace(-2, 2, 100), + "multiplicative_input", + ), + ( + control.TransferFunction([1], [0.5, 1]), + [ + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), + ], + np.logspace(-2, 2, 100), + "multiplicative_output", + ), + ( + control.TransferFunction([1], [0.5, 1]), + [ + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), + ], + np.logspace(-2, 2, 100), + "inverse_additive", + ), + ( + control.TransferFunction([1], [0.5, 1]), + [ + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), + ], + np.logspace(-2, 2, 100), + "inverse_multiplicative_input", + ), + ( + control.TransferFunction([1], [0.5, 1]), + [ + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), + ], + np.logspace(-2, 2, 100), + "inverse_multiplicative_output", ), ( control.TransferFunction( @@ -716,9 +979,7 @@ class TestFitUncertaintyWeight: ), ], np.logspace(-1, 1.5, 100), - "diagonal", - "diagonal", - 4, + "additive", ), ( control.TransferFunction( @@ -764,240 +1025,7 @@ class TestFitUncertaintyWeight: ), ], np.logspace(-1, 1.5, 100), - "diagonal", - "diagonal", - [4, 4], - ), - ], - ) - def test_fit_uncertainty_weight( - self, - ndarrays_regression, - sys_nom, - sys_offnom_list, - omega, - weight_left_structure, - weight_right_structure, - fit_order, - ): - """Regression test :func:`fit_uncertainty_weight`.""" - # Frequency response of systems - frequency_response_nom = control.frequency_response( - sys_nom, omega, squeeze=False - ) - frequency_response_offnom_list = control.frequency_response( - sys_offnom_list, omega, squeeze=False - ) - # Complex frequency response of systems - complex_response_nom = frequency_response_nom.complex.transpose(2, 0, 1) - complex_response_offnom_list = [] - for frequency_response_offnom in frequency_response_offnom_list: - complex_response_offnom_list.append( - frequency_response_offnom.complex.transpose(2, 0, 1) - ) - complex_response_offnom_list = np.array( - complex_response_offnom_list, dtype=complex - ) - - # Uncertainty residual computation - uncertainty_model_list = ["multiplicative_input"] - complex_response_residual_dict = dkpy.compute_uncertainty_residual_response( - complex_response_nom, - complex_response_offnom_list, - uncertainty_model_list, - ) - - # Optimal uncertainty weight - complex_response_weight_left, complex_response_weight_right = ( - dkpy.compute_uncertainty_weight_response( - complex_response_residual_dict["multiplicative_input"], - weight_left_structure, - weight_right_structure, - ) - ) - - # Overbounding transfer function fit - weight_left_fit = dkpy.fit_uncertainty_weight( - complex_response_weight_left, - omega, - fit_order, - "left", - weight_left_structure, - ) - weight_right_fit = dkpy.fit_uncertainty_weight( - complex_response_weight_right, - omega, - fit_order, - "right", - weight_right_structure, - ) - - # Uncertainty weight fit frequency response - frequency_response_weight_left_fit = control.frequency_response( - weight_left_fit, omega, squeeze=False - ) - frequency_response_weight_right_fit = control.frequency_response( - weight_right_fit, omega, squeeze=False - ) - complex_response_weight_left_fit = ( - frequency_response_weight_left_fit.complex.transpose(2, 0, 1) - ) - complex_response_weight_right_fit = ( - frequency_response_weight_right_fit.complex.transpose(2, 0, 1) - ) - complex_response_weight_fit_dict = { - "left": complex_response_weight_left_fit, - "right": complex_response_weight_right_fit, - } - - # Regression testing - ndarrays_regression.check( - complex_response_weight_fit_dict, - default_tolerance=dict(atol=1e-2, rtol=1e-2), - ) - - -class TestConvertFrequencyResponseDataToArray: - @pytest.mark.parametrize( - "sys, omega", - [ - ( - control.TransferFunction([1], [0.5, 1]), - np.logspace(-2, 2, 100), - ), - ( - control.TransferFunction([1], [1, 2 * 0.5 * 1, 1**2]), - np.logspace(-1, 1, 100), - ), - ( - control.TransferFunction( - [ - [[1], [3]], - [[2], [1]], - ], - [ - [[1, 2 * 0.5 * 1, 1**2], [0.5, 1]], - [[1, 1], [1, 2 * 0.3 * 5, 5**2]], - ], - ), - np.logspace(-1, 1.5, 100), - ), - ], - ) - def test_convert_frequency_response_data_to_array(self, sys, omega): - # Desired frequency response array shape - complex_response_shape_true = (omega.size, sys.noutputs, sys.ninputs) - - # Evaluate frequency response - frequency_response = control.frequency_response(sys, omega) - - # Convert frequency response data - complex_response = ( - dkpy.uncertainty_characterization._convert_frequency_response_data_to_array( - frequency_response - ) - ) - - # Verify type - assert isinstance(complex_response, np.ndarray) - # Verify dimensions - assert complex_response.shape == complex_response_shape_true - # Verify complex datatype - assert np.all(np.iscomplex(complex_response)) - # Verify that the function does not affect responses in the correct format - assert np.all( - np.isclose( - complex_response, - dkpy.uncertainty_characterization._convert_frequency_response_data_to_array( - complex_response - ), - ), - ) - - @pytest.mark.parametrize( - "complex_response", - [ - np.ones(100, dtype=complex), - np.ones((100, 5), dtype=complex), - np.ones((100, 5, 5, 5), dtype=complex), - ], - ) - def test_convert_frequency_response_data_to_array_error(self, complex_response): - with pytest.raises(ValueError): - complex_response = dkpy.uncertainty_characterization._convert_frequency_response_data_to_array( - complex_response - ) - - -class TestComputeUncertaintyMeasureResponse: - @pytest.mark.parametrize( - "nominal, offnominal, omega, uncertainty_model", - [ - ( - control.TransferFunction([1], [0.5, 1]), - [ - control.TransferFunction([1], [0.3, 1]), - control.TransferFunction([1], [0.4, 1]), - control.TransferFunction([1], [0.6, 1]), - control.TransferFunction([1], [0.7, 1]), - ], - np.logspace(-2, 2, 100), - "additive", - ), - ( - control.TransferFunction([1], [0.5, 1]), - [ - control.TransferFunction([1], [0.3, 1]), - control.TransferFunction([1], [0.4, 1]), - control.TransferFunction([1], [0.6, 1]), - control.TransferFunction([1], [0.7, 1]), - ], - np.logspace(-2, 2, 100), - "multiplicative_input", - ), - ( - control.TransferFunction([1], [0.5, 1]), - [ - control.TransferFunction([1], [0.3, 1]), - control.TransferFunction([1], [0.4, 1]), - control.TransferFunction([1], [0.6, 1]), - control.TransferFunction([1], [0.7, 1]), - ], - np.logspace(-2, 2, 100), - "multiplicative_output", - ), - ( - control.TransferFunction([1], [0.5, 1]), - [ - control.TransferFunction([1], [0.3, 1]), - control.TransferFunction([1], [0.4, 1]), - control.TransferFunction([1], [0.6, 1]), - control.TransferFunction([1], [0.7, 1]), - ], - np.logspace(-2, 2, 100), - "inverse_additive", - ), - ( - control.TransferFunction([1], [0.5, 1]), - [ - control.TransferFunction([1], [0.3, 1]), - control.TransferFunction([1], [0.4, 1]), - control.TransferFunction([1], [0.6, 1]), - control.TransferFunction([1], [0.7, 1]), - ], - np.logspace(-2, 2, 100), - "inverse_multiplicative_input", - ), - ( - control.TransferFunction([1], [0.5, 1]), - [ - control.TransferFunction([1], [0.3, 1]), - control.TransferFunction([1], [0.4, 1]), - control.TransferFunction([1], [0.6, 1]), - control.TransferFunction([1], [0.7, 1]), - ], - np.logspace(-2, 2, 100), - "inverse_multiplicative_output", + "multiplicative_input", ), ( control.TransferFunction( @@ -1043,7 +1071,7 @@ class TestComputeUncertaintyMeasureResponse: ), ], np.logspace(-1, 1.5, 100), - "additive", + "multiplicative_output", ), ( control.TransferFunction( @@ -1089,7 +1117,7 @@ class TestComputeUncertaintyMeasureResponse: ), ], np.logspace(-1, 1.5, 100), - "multiplicative_input", + "inverse_additive", ), ( control.TransferFunction( @@ -1135,7 +1163,7 @@ class TestComputeUncertaintyMeasureResponse: ), ], np.logspace(-1, 1.5, 100), - "multiplicative_output", + "inverse_multiplicative_input", ), ( control.TransferFunction( @@ -1181,7 +1209,92 @@ class TestComputeUncertaintyMeasureResponse: ), ], np.logspace(-1, 1.5, 100), - "inverse_additive", + "inverse_multiplicative_output", + ), + ], + ) + def test_compute_uncertainty_measure_response( + self, + ndarrays_regression, + nominal, + offnominal, + omega, + uncertainty_model, + ): + """Regression test for :func:`compute_uncertainty_measure_response`.""" + + # Frequency response of systems + frd_nominal = control.frequency_response(nominal, omega, squeeze=False) + frd_offnominal_list = control.frequency_response( + offnominal, omega, squeeze=False + ) + + # Uncertainty residual computation + uncertainty_model_list = [uncertainty_model] + complex_residual_dict = dkpy.compute_uncertainty_residual_response( + frd_nominal, + frd_offnominal_list, + uncertainty_model_list, + ) + + measure_dict = {} + for weight_structure in ["scalar", "diagonal", "full"]: + # Uncertainty weight computation + complex_weight_left, complex_weight_right = ( + dkpy.compute_uncertainty_weight_response( + complex_residual_dict[uncertainty_model], + weight_structure, + weight_structure, + ) + ) + + # Uncertainty measure computation + measure = dkpy.compute_uncertainty_measure_response( + frd_nominal, + complex_weight_left, + complex_weight_right, + uncertainty_model, + ) + + measure_dict[weight_structure] = measure + + ndarrays_regression.check( + measure_dict, + default_tolerance=dict(atol=1e-2, rtol=1e-2), + ) + + +class TestFitUncertaintyWeight: + """Test :func:`fit_uncertainty_weight`.""" + + @pytest.mark.parametrize( + "sys_nom, sys_offnom_list, omega, weight_left_structure, weight_right_structure, fit_order", + [ + ( + control.TransferFunction([1], [0.5, 1]), + [ + control.TransferFunction([1], [0.3, 1]), + control.TransferFunction([1], [0.4, 1]), + control.TransferFunction([1], [0.6, 1]), + control.TransferFunction([1], [0.7, 1]), + ], + np.logspace(-2, 2, 100), + "diagonal", + "diagonal", + 4, + ), + ( + control.TransferFunction([1], [1, 2 * 0.5 * 1, 1**2]), + [ + control.TransferFunction([1], [1, 2 * 0.3 * 1.5, 1.5**2]), + control.TransferFunction([1], [1, 2 * 0.7 * 1.3, 1.3**2]), + control.TransferFunction([1], [1, 2 * 0.2 * 0.9, 0.9**2]), + control.TransferFunction([1], [1, 2 * 0.9 * 0.7, 0.7**2]), + ], + np.logspace(-1, 1, 100), + "diagonal", + "diagonal", + 4, ), ( control.TransferFunction( @@ -1227,7 +1340,9 @@ class TestComputeUncertaintyMeasureResponse: ), ], np.logspace(-1, 1.5, 100), - "inverse_multiplicative_input", + "diagonal", + "diagonal", + 4, ), ( control.TransferFunction( @@ -1273,61 +1388,171 @@ class TestComputeUncertaintyMeasureResponse: ), ], np.logspace(-1, 1.5, 100), - "inverse_multiplicative_output", + "diagonal", + "diagonal", + [4, 4], ), ], ) - def test_compute_uncertainty_measure_response( + def test_fit_uncertainty_weight( self, ndarrays_regression, - nominal, - offnominal, + sys_nom, + sys_offnom_list, omega, - uncertainty_model, + weight_left_structure, + weight_right_structure, + fit_order, ): - """Regression test for :func:`compute_uncertainty_measure_response`.""" - + """Regression test :func:`fit_uncertainty_weight`.""" # Frequency response of systems - frd_nominal = control.frequency_response(nominal, omega, squeeze=False) - frd_offnominal_list = control.frequency_response( - offnominal, omega, squeeze=False + frequency_response_nom = control.frequency_response( + sys_nom, omega, squeeze=False + ) + frequency_response_offnom_list = control.frequency_response( + sys_offnom_list, omega, squeeze=False + ) + # Complex frequency response of systems + complex_response_nom = frequency_response_nom.complex.transpose(2, 0, 1) + complex_response_offnom_list = [] + for frequency_response_offnom in frequency_response_offnom_list: + complex_response_offnom_list.append( + frequency_response_offnom.complex.transpose(2, 0, 1) + ) + complex_response_offnom_list = np.array( + complex_response_offnom_list, dtype=complex ) # Uncertainty residual computation - uncertainty_model_list = [uncertainty_model] - complex_residual_dict = dkpy.compute_uncertainty_residual_response( - frd_nominal, - frd_offnominal_list, + uncertainty_model_list = ["multiplicative_input"] + complex_response_residual_dict = dkpy.compute_uncertainty_residual_response( + complex_response_nom, + complex_response_offnom_list, uncertainty_model_list, ) - measure_dict = {} - for weight_structure in ["scalar", "diagonal", "full"]: - # Uncertainty weight computation - complex_weight_left, complex_weight_right = ( - dkpy.compute_uncertainty_weight_response( - complex_residual_dict[uncertainty_model], - weight_structure, - weight_structure, - ) + # Optimal uncertainty weight + complex_response_weight_left, complex_response_weight_right = ( + dkpy.compute_uncertainty_weight_response( + complex_response_residual_dict["multiplicative_input"], + weight_left_structure, + weight_right_structure, ) + ) - # Uncertainty measure computation - measure = dkpy.compute_uncertainty_measure_response( - frd_nominal, - complex_weight_left, - complex_weight_right, - uncertainty_model, - ) + # Overbounding transfer function fit + weight_left_fit = dkpy.fit_uncertainty_weight( + complex_response_weight_left, + omega, + fit_order, + "left", + weight_left_structure, + ) + weight_right_fit = dkpy.fit_uncertainty_weight( + complex_response_weight_right, + omega, + fit_order, + "right", + weight_right_structure, + ) - measure_dict[weight_structure] = measure + # Uncertainty weight fit frequency response + frequency_response_weight_left_fit = control.frequency_response( + weight_left_fit, omega, squeeze=False + ) + frequency_response_weight_right_fit = control.frequency_response( + weight_right_fit, omega, squeeze=False + ) + complex_response_weight_left_fit = ( + frequency_response_weight_left_fit.complex.transpose(2, 0, 1) + ) + complex_response_weight_right_fit = ( + frequency_response_weight_right_fit.complex.transpose(2, 0, 1) + ) + complex_response_weight_fit_dict = { + "left": complex_response_weight_left_fit, + "right": complex_response_weight_right_fit, + } + # Regression testing ndarrays_regression.check( - measure_dict, + complex_response_weight_fit_dict, default_tolerance=dict(atol=1e-2, rtol=1e-2), ) +class TestConvertFrequencyResponseDataToArray: + @pytest.mark.parametrize( + "sys, omega", + [ + ( + control.TransferFunction([1], [0.5, 1]), + np.logspace(-2, 2, 100), + ), + ( + control.TransferFunction([1], [1, 2 * 0.5 * 1, 1**2]), + np.logspace(-1, 1, 100), + ), + ( + control.TransferFunction( + [ + [[1], [3]], + [[2], [1]], + ], + [ + [[1, 2 * 0.5 * 1, 1**2], [0.5, 1]], + [[1, 1], [1, 2 * 0.3 * 5, 5**2]], + ], + ), + np.logspace(-1, 1.5, 100), + ), + ], + ) + def test_convert_frequency_response_data_to_array(self, sys, omega): + # Desired frequency response array shape + complex_response_shape_true = (omega.size, sys.noutputs, sys.ninputs) + + # Evaluate frequency response + frequency_response = control.frequency_response(sys, omega) + + # Convert frequency response data + complex_response = ( + dkpy.uncertainty_characterization._convert_frequency_response_data_to_array( + frequency_response + ) + ) + + # Verify type + assert isinstance(complex_response, np.ndarray) + # Verify dimensions + assert complex_response.shape == complex_response_shape_true + # Verify complex datatype + assert np.all(np.iscomplex(complex_response)) + # Verify that the function does not affect responses in the correct format + assert np.all( + np.isclose( + complex_response, + dkpy.uncertainty_characterization._convert_frequency_response_data_to_array( + complex_response + ), + ), + ) + + @pytest.mark.parametrize( + "complex_response", + [ + np.ones(100, dtype=complex), + np.ones((100, 5), dtype=complex), + np.ones((100, 5, 5, 5), dtype=complex), + ], + ) + def test_convert_frequency_response_data_to_array_error(self, complex_response): + with pytest.raises(ValueError): + complex_response = dkpy.uncertainty_characterization._convert_frequency_response_data_to_array( + complex_response + ) + + class TestConvertFrequencyResponseListToArray: @pytest.mark.parametrize( "sys_list, omega",