diff --git a/src/state_abstraction.py b/src/state_abstraction.py new file mode 100644 index 0000000000000000000000000000000000000000..d6b5b4f90c0edcdad985fd84af6866c0b1318208 --- /dev/null +++ b/src/state_abstraction.py @@ -0,0 +1,321 @@ +## This file is part of the simulative evaluation for the qronos observer abstractions. +## Copyright (C) 2022-2023 Tim Rheinfels <tim.rheinfels@fau.de> +## See https://gitlab.cs.fau.de/qronos-state-abstractions/simulation +## +## Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +## +## 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +## +## 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +## +## 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +### +### @file state_abstraction.py +### +### @brief Provides a class for (observer) state abstractions as described in @cite ECRTS23-Rheinfels +### +### @author Tim Rheinfels <tim.rheinfels@fau.de> +### + +import numpy as np + +import analysis +from ellipsoid import Ellipsoid +from system_model import SystemModel +from util import Serializable + +### +### @brief Encapsulates a (observer) state abstraction as described in @cite ECRTS23-Rheinfels, equation 10 +### +class StateAbstraction(Serializable): + + ### + ### @brief Encapsualtes the state abstraction of a single @p underlying_mode of a switched system + ### + class Mode(Serializable): + + ### + ### @brief Constructor + ### + ### @param underlying_mode A single @ref system_model.SystemModel.Mode + ### @param L Observer gain matrix @f$ L \in \R^{n_x \times n_y} @f$ or None to omit observer by setting @f$ L = 0 @f$ + ### @param rho Coefficient @f$ \rho @f$ computed by equation (10) / @ref analysis.abstraction_mode_coefficients + ### @param gamma Coefficient @f$ \gamma @f$ computed by equation (10) / @ref analysis.abstraction_mode_coefficients + ### @param beta Coefficient @f$ \beta @f$ computed by equation (10) / @ref analysis.abstraction_mode_coefficients + ### @param delta Coefficient @f$ \delta @f$ computed by equation (10) / @ref analysis.abstraction_mode_coefficients + ### @param v_star Coefficient @f$ v^\star @f$ computed by equation (10) / @ref analysis.abstraction_mode_coefficients + ### @param v_inf Coefficient @f$ v_\infty @f$ computed by @ref analysis.abstraction_mode_coefficients + def __init__(self, underlying_mode, L, rho, gamma, beta, delta, v_star, v_inf): + # Check parameters + assert(isinstance(underlying_mode, SystemModel.Mode)) + if L is None: + L = np.zeros((underlying_mode.n_x, underlying_mode.n_y)) + assert(isinstance(L, np.ndarray)) + assert(L.ndim == 2) + assert(L.shape[0] == underlying_mode.n_x) + assert(L.shape[1] == underlying_mode.n_y) + assert(rho > 0.0) + assert(gamma >= 0.0) + assert(beta >= 0.0) + assert(delta >= 0.0) + + self._underlying_mode = underlying_mode + self._L = L + self._rho = rho + self._gamma = gamma + self._beta = beta + self._delta = delta + self._v_star = v_star + self._v_inf = v_inf + + ### + ### @brief Getter for the mode's name + ### + ### @returns Mode's name + ### + @property + def name(self): + return self._underlying_mode.name + + ### + ### @brief Getter for the underlying @ref system_model.SystemModel.Mode object + ### + ### @returns The underlying @ref system_model.SystemModel.Mode object + ### + @property + def underlying_mode(self): + return self._underlying_mode + + ### + ### @brief Getter for the observer gain matrix @f$ L \in \R^{n_x \times n_y} @f$ + ### + ### @returns Observer gain matrix @f$ L \in \R^{n_x \times n_y} @f$ + ### + @property + def L(self): + return self._L + + ### + ### @brief Getter for the abstraction mode's dynamics coefficient @f$ \rho @f$ + ### + ### @returns Abstraction mode's dynamics coefficient @f$ \rho @f$ + ### + @property + def rho(self): + return self._rho + + ### + ### @brief Getter for the abstraction mode's worst-case addition dynamics coefficient @f$ \gamma @f$ + ### + ### @returns Abstraction mode's dynamics coefficient @f$ \gamma @f$ + ### + @property + def gamma(self): + return self._gamma + + ### + ### @brief Getter for the abstraction mode's disturbance input coefficient @f$ \beta @f$ + ### + ### @returns Abstraction mode's disturbance input @f$ \beta @f$ + ### + @property + def beta(self): + return self._beta + + ### + ### @brief Getter for the abstraction mode's worst-case additional disturbance input coefficient @f$ \delta @f$ + ### + ### @returns Abstraction mode's worst-case additional disturbance input @f$ \delta @f$ + ### + @property + def delta(self): + return self._delta + + ### + ### @brief Getter for the abstraction mode's highest admissible abstraction value @f$ v^\star @f$ + ### + ### @returns Abstraction mode's highest admissible abstraction value @f$ v^\star @f$ + ### + @property + def v_star(self): + return self._v_star + + ### + ### @brief Getter for the abstraction mode's worst-case stationary value @f$ v_\infty @f$ + ### + ### @returns Abstraction mode's worst-case stationary value @f$ v_\infty @f$ + ### + @property + def v_inf(self): + return self._v_inf + + ### + ### @brief Converts the mode's data into a dictionary + ### + ### @returns Dictionary characterizing the mode + ### + ### @see util.Serializable + ### + def to_dict(self): + return { + 'name': self.name, + 'L': self.L, + 'rho': self.rho, + 'gamma': self.gamma, + 'rho+gamma': (self.rho+self.gamma), + 'beta': self.beta, + 'delta': self.delta, + 'beta+delta': (self.beta+self.delta), + 'v_star': self._v_star, + 'v_inf': self._v_inf, + } + + ### + ### @brief Constructor + ### + ### @param name Name to assign to the abstraction + ### @param underlying_system The @ref system_model.SystemModel to compute the abstraction for + ### @param E_P Analysis ellipsoid.Ellipsoid + ### @param L List of observer gain matrices. Set (individual component) to None to omit the (corresponding) observer gains + ### @param parametrization_time Time it took to run the parametrization or None to omit + ### + def __init__(self, name, underlying_system, E_P, L=None, parametrization_time=None): + # Check parameters + assert(isinstance(name, str)) + assert(isinstance(underlying_system, SystemModel)) + assert(isinstance(E_P, Ellipsoid)) + assert(E_P.centered) + assert(not E_P.degenerate) + assert(underlying_system.n_x == E_P.n) + if L is None: + L = [None] * underlying_system.n_Sigma + assert(len(L) == underlying_system.n_Sigma) + + if underlying_system.E_X_0.degenerate or underlying_system.E_S.degenerate: + raise ValueError('State abstractions require non-degenerate ellipsoids E_X_0 and E_S') + + if not (underlying_system.E_X_0.centered and underlying_system.E_S.centered): + raise ValueError('State abstractions analysis with non-centered ellipsoids E_X_0 and E_S not yet supported') + + self._name = name + self._E_P = E_P + self._parametrization_time = parametrization_time + self._condition_number = np.linalg.cond(E_P.P) + self._underlying_system = underlying_system + + # Global abstraction parameters + self._alpha, self._v_max = analysis.abstraction_global_coefficients( + underlying_system.E_X_0.P, + underlying_system.C_s, + underlying_system.E_S.P, + E_P.P, + ) + + # Per-mode Abstraction Parameters + self._modes = [] + for i, underlying_mode in enumerate(underlying_system.modes): + rho, gamma, beta, delta, v_star, v_inf = analysis.abstraction_mode_coefficients(underlying_mode.A, underlying_mode.G, underlying_mode.C, underlying_mode.H, L[i], underlying_mode.E_D.P, underlying_mode.E_Z.P, E_P.P, self._v_max) + self._modes.append(StateAbstraction.Mode(underlying_mode, L[i], rho, gamma, beta, delta, v_star, v_inf)) + + ### + ### @brief Getter for the abstraction's name + ### + ### @returns Abstraction's name + ### + @property + def name(self): + return self._name + + ### + ### @brief Getter for the number of modes @f$ n_\Sigma @f$ + ### + ### @returns Number of modes @f$ n_\Sigma @f$ + ### + @property + def n_Sigma(self): + return self._underlying_system.n_Sigma + + ### + ### @brief Getter for the @ref system_model.SystemModel the abstraction was paramertrized for + ### + ### @returns The @ref system_model.SystemModel the abstraction was paramertrized for + ### + @property + def underlying_system(self): + return self._underlying_system + + ### + ### @brief Getter for the list of the @f$ n_\Sigma @f$ abstraction modes + ### + ### @returns List of the @f$ n_\Sigma @f$ abstraction modes + ### + @property + def modes(self): + return self._modes + + ### + ### @brief Getter for the analysis @ref ellipsoid.Ellipsoid + ### + ### @returns The analysis @ref ellipsoid.Ellipsoid + ### + @property + def E_P(self): + return self._E_P + + ### + ### @brief Getter for the initial value coefficient @f$ \alpha @f$ computed by @ref analysis.abstraction_global_coefficients + ### + ### @returns The initial value coefficient @f$ \alpha @f$ computed by @ref analysis.abstraction_global_coefficients + ### + @property + def alpha(self): + return self._alpha + + ### + ### @brief Getter for the maximum admissible abstraction value @f$ v_{max} @f$ computed by @ref analysis.abstraction_global_coefficients + ### + ### @returns The maximum admissible abstraction value @f$ v_{max} @f$ computed by @ref analysis.abstraction_global_coefficients + ### + @property + def v_max(self): + return self._v_max + + ### + ### @brief Getter for the parametrization time + ### + ### @returns The parametrization time + ### + @property + def parametrization_time(self): + return self._parametrization_time + + ### + ### @brief Getter for the analysis ellipsoid's condition number + ### + ### @returns The analysis ellipsoid's condition number + ### + @property + def condition_number(self): + return self._condition_number + + ### + ### @brief Converts the abstraction's data into a dictionary + ### + ### @returns Dictionary characterizing the abstraction + ### + ### @see util.Serializable + ### + def to_dict(self): + return { + 'name': self.name, + 'n_Sigma': self.n_Sigma, + 'modes': [mode.to_dict() for mode in self.modes], + 'E_P': self.E_P.to_dict(), + 'alpha': self.alpha, + 'v_max': self.v_max, + 'parametrization_time': self.parametrization_time, + 'condition_number': self.condition_number, + } diff --git a/src/test/state_abstraction/__init__.py b/src/test/state_abstraction/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/test/state_abstraction/constructor.py b/src/test/state_abstraction/constructor.py new file mode 100644 index 0000000000000000000000000000000000000000..92d8efa5eb913a056dce4f9a111cb6402f0a1ab6 --- /dev/null +++ b/src/test/state_abstraction/constructor.py @@ -0,0 +1,197 @@ +## This file is part of the simulative evaluation for the qronos observer abstractions. +## Copyright (C) 2022-2023 Tim Rheinfels <tim.rheinfels@fau.de> +## See https://gitlab.cs.fau.de/qronos-state-abstractions/simulation +## +## Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +## +## 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +## +## 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +## +## 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +### +### @file test/state_abstraction/constructor.py +### +### @brief Provides unit tests for @ref state_abstraction.StateAbstraction.__init__ +### +### @author Tim Rheinfels <tim.rheinfels@fau.de> +### + +import numpy as np +import scipy as sp +import scipy.linalg +import unittest + +import analysis +from ellipsoid import Ellipsoid +from state_abstraction import StateAbstraction +from system_model import SystemModel + +### +### @brief Encapsulates the unit tests for @ref state_abstraction.StateAbstraction.__init__ +### +class Test(unittest.TestCase): + + ### + ### @brief Sets up the same rng and @ref system_model.SystemModel instance for all tests + ### + def setUp(self): + self.rng = np.random.default_rng(0) + self.underlying_system = SystemModel( + 'test_system', + [ + SystemModel.Mode( + 'test_mode_1', + np.array([[1, 2], [3, 4]]), + np.array([[5, 6, 7], [8, 9, 10]]), + Ellipsoid(np.diag([11, 12, 13])), + np.array([[14, 15]]), + np.array([[16]]), + Ellipsoid(np.diag([17])), + ), + SystemModel.Mode( + 'test_mode_2', + np.array([[18, 19], [20, 21]]), + np.array([[22], [23]]), + Ellipsoid(np.diag([24])), + np.array([[25, 26], [27, 28]]), + np.array([[29, 30, 31], [32, 33, 34]]), + Ellipsoid(np.diag([35, 36, 37])), + ), + ], + Ellipsoid(np.diag([38, 39])), + np.array([[50, 51], [52, 53], [54, 55]]), + Ellipsoid(np.diag([56, 57, 58])), + ) + + self.L = [ + None, + [None] * 2, + [ + np.array([[70], [71]]), + None, + ], + [ + None, + np.array([[72, 73], [74, 75]]) + ], + [ + np.array([[76], [77]]), + np.array([[78, 79], [80, 81]]) + ], + ] + + self.E_P = Ellipsoid(np.diag([88, 89])) + + self.parametization_time = 123.4 + + ### + ### @brief Checks that passing valid data to the constructor yields a correctly initialized @ref state_abstraction.StateAbstraction instance + ### + def test_valid(self): + alpha, v_max = analysis.abstraction_global_coefficients( + self.underlying_system.E_X_0.P, + self.underlying_system.C_s, + self.underlying_system.E_S.P, + self.E_P.P, + ) + + for L in self.L: + abstraction = StateAbstraction( + 'test_abstraction', + self.underlying_system, + self.E_P, + L, + self.parametization_time, + ) + + if L is None: + L = [None] * 2 + + if L[0] is None: + L[0] = np.zeros((2, 1)) + + if L[1] is None: + L[1] = np.zeros((2, 2)) + + self.assertEqual(abstraction.name, 'test_abstraction') + self.assertEqual(abstraction.underlying_system, self.underlying_system) + self.assertEqual(abstraction.n_Sigma, 2) + self.assertEqual(len(abstraction.modes), 2) + self.assertEqual(abstraction.E_P, self.E_P) + self.assertEqual(abstraction.alpha, alpha) + self.assertEqual(abstraction.v_max, v_max) + self.assertEqual(abstraction.parametrization_time, self.parametization_time) + self.assertAlmostEqual(abstraction.condition_number, np.linalg.cond(self.E_P.P)) + for i, underlying_mode in enumerate(self.underlying_system.modes): + self.assertTrue(isinstance(abstraction.modes[i], StateAbstraction.Mode)) + self.assertTrue(abstraction.modes[i].underlying_mode, underlying_mode) + + d = abstraction.to_dict() + self.assertEqual(set(d.keys()), set(('name', 'n_Sigma', 'modes', 'E_P', 'alpha', 'v_max', 'parametrization_time', 'condition_number'))) + self.assertEqual(d['name'], 'test_abstraction') + self.assertEqual(d['n_Sigma'], self.underlying_system.n_Sigma) + self.assertEqual(d['E_P'], self.E_P.to_dict()) + self.assertEqual(d['alpha'], alpha) + self.assertEqual(d['v_max'], v_max) + self.assertEqual(d['parametrization_time'], self.parametization_time) + self.assertEqual(d['condition_number'], np.linalg.cond(self.E_P.P)) + + ### + ### @brief Checks that passing degenerate ellipsoids yields an exception + ### + def test_invalid_degenerate(self): + X_0 = self.underlying_system.E_X_0.P + S = self.underlying_system.E_S.P + P = self.E_P.P.copy() + + for A_x in (np.eye(2), np.diag([1, 0])): + self.underlying_system._E_X_0 = Ellipsoid(A_x @ X_0 @ A_x.T) + for A_s in (np.eye(3), np.diag([1, 0, 1])): + self.underlying_system._E_S = Ellipsoid(A_s @ S @ A_s.T) + for A_p in (np.eye(2), np.diag([1, 0])): + self.E_P = Ellipsoid(A_p @ P @ A_p.T) + + if not (self.underlying_system.E_X_0.degenerate or self.underlying_system.E_S.degenerate or self.E_P.degenerate): + continue + + for L in self.L: + with self.assertRaises(Exception): + abstraction = StateAbstraction( + 'test_abstraction', + self.underlying_system, + self.E_P, + L, + None, + ) + + ### + ### @brief Checks that passing non-centered ellipsoids yields an exception + ### + def test_invalid_non_centered(self): + X_0 = self.underlying_system.E_X_0.P + S = self.underlying_system.E_S.P + P = self.E_P.P.copy() + + for x_c in (np.zeros(2), np.ones(2)): + self.underlying_system._E_X_0 = Ellipsoid(X_0, x_c) + for s_c in (np.zeros(3), np.ones(3)): + self.underlying_system._E_S = Ellipsoid(S, s_c) + for s_p in (np.zeros(2), np.ones(2)): + self.E_P = Ellipsoid(P, s_p) + + if self.underlying_system.E_X_0.centered and self.underlying_system.E_S.centered and self.E_P.centered: + continue + + for L in self.L: + with self.assertRaises(Exception): + abstraction = StateAbstraction( + 'test_abstraction', + self.underlying_system, + self.E_P, + L, + None, + ) diff --git a/src/test/state_abstraction/mode_constructor.py b/src/test/state_abstraction/mode_constructor.py new file mode 100644 index 0000000000000000000000000000000000000000..86754a3598fd4f201df75c7a4dc0602f269fda3b --- /dev/null +++ b/src/test/state_abstraction/mode_constructor.py @@ -0,0 +1,99 @@ +## This file is part of the simulative evaluation for the qronos observer abstractions. +## Copyright (C) 2022-2023 Tim Rheinfels <tim.rheinfels@fau.de> +## See https://gitlab.cs.fau.de/qronos-state-abstractions/simulation +## +## Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +## +## 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +## +## 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +## +## 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +### +### @file test/state_abstraction/mode_constructor.py +### +### @brief Provides unit tests for @ref state_abstraction.StateAbstraction.Mode.__init__ +### +### @author Tim Rheinfels <tim.rheinfels@fau.de> +### + +import numpy as np +import scipy as sp +import scipy.linalg +import unittest + +from ellipsoid import Ellipsoid +from state_abstraction import StateAbstraction +from system_model import SystemModel + +### +### @brief Encapsulates the unit tests for @ref state_abstraction.StateAbstraction.Mode.__init__ +### +class Test(unittest.TestCase): + + ### + ### @brief Sets up the same rng and @ref system_model.SystemModel.Mode instance for all tests + ### + def setUp(self): + self.rng = np.random.default_rng(0) + self.underlying_mode = SystemModel.Mode( + 'test_mode', + np.array([[1, 2], [3, 4]]), + np.array([[5, 6, 7], [8, 9, 10]]), + Ellipsoid(np.diag([11, 12, 13])), + np.array([[14, 15]]), + np.array([[16]]), + Ellipsoid(np.diag([17])), + ) + + ### + ### @brief Checks that passing valid data to the constructor yields a correctly initialized @ref state_abstraction.StateAbstraction.Mode instance + ### + def test_valid(self): + rho = 18.0 + gamma = 19.0 + beta = 20.0 + delta = 21.0 + v_star = 22.0 + v_inf = 23.0 + + for L in (None, np.array([[17], [18]])): + mode = StateAbstraction.Mode( + self.underlying_mode, + L, + rho, + gamma, + beta, + delta, + v_star, + v_inf, + ) + + if L is None: + L = np.zeros((2, 1)) + + self.assertEqual(mode.name, self.underlying_mode.name) + self.assertEqual(mode.underlying_mode, self.underlying_mode) + self.assertTrue(np.all(mode.L == L)) + self.assertEqual(mode.rho, rho) + self.assertEqual(mode.gamma, gamma) + self.assertEqual(mode.beta, beta) + self.assertEqual(mode.delta, delta) + self.assertEqual(mode.v_star, v_star) + self.assertEqual(mode.v_inf, v_inf) + + d = mode.to_dict() + self.assertEqual(set(d.keys()), set(('name', 'L', 'rho', 'gamma', 'rho+gamma', 'beta', 'delta', 'beta+delta', 'v_star', 'v_inf'))) + self.assertEqual(d['name'], self.underlying_mode.name) + self.assertTrue(np.all(d['L'] == L)) + self.assertEqual(d['rho'], rho) + self.assertEqual(d['gamma'], gamma) + self.assertEqual(d['rho+gamma'], rho+gamma) + self.assertEqual(d['beta'], beta) + self.assertEqual(d['delta'], delta) + self.assertEqual(d['beta+delta'], beta+delta) + self.assertEqual(d['v_star'], v_star) + self.assertEqual(d['v_inf'], v_inf)