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)