pathsim 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. pathsim/__init__.py +3 -0
  2. pathsim/blocks/__init__.py +14 -0
  3. pathsim/blocks/_block.py +209 -0
  4. pathsim/blocks/adder.py +30 -0
  5. pathsim/blocks/amplifier.py +34 -0
  6. pathsim/blocks/delay.py +70 -0
  7. pathsim/blocks/differentiator.py +70 -0
  8. pathsim/blocks/function.py +82 -0
  9. pathsim/blocks/integrator.py +66 -0
  10. pathsim/blocks/lti.py +155 -0
  11. pathsim/blocks/multiplier.py +30 -0
  12. pathsim/blocks/ode.py +86 -0
  13. pathsim/blocks/rf/__init__.py +4 -0
  14. pathsim/blocks/rf/filters.py +169 -0
  15. pathsim/blocks/rf/noise.py +218 -0
  16. pathsim/blocks/rf/sources.py +163 -0
  17. pathsim/blocks/rf/wienerhammerstein.py +338 -0
  18. pathsim/blocks/rng.py +57 -0
  19. pathsim/blocks/scope.py +224 -0
  20. pathsim/blocks/sources.py +71 -0
  21. pathsim/blocks/spectrum.py +316 -0
  22. pathsim/connection.py +112 -0
  23. pathsim/simulation.py +652 -0
  24. pathsim/solvers/__init__.py +25 -0
  25. pathsim/solvers/_solver.py +403 -0
  26. pathsim/solvers/bdf.py +240 -0
  27. pathsim/solvers/dirk2.py +101 -0
  28. pathsim/solvers/dirk3.py +86 -0
  29. pathsim/solvers/esdirk32.py +131 -0
  30. pathsim/solvers/esdirk4.py +99 -0
  31. pathsim/solvers/esdirk43.py +139 -0
  32. pathsim/solvers/esdirk54.py +141 -0
  33. pathsim/solvers/esdirk85.py +200 -0
  34. pathsim/solvers/euler.py +81 -0
  35. pathsim/solvers/rk4.py +61 -0
  36. pathsim/solvers/rkbs32.py +101 -0
  37. pathsim/solvers/rkck54.py +108 -0
  38. pathsim/solvers/rkdp54.py +111 -0
  39. pathsim/solvers/rkdp87.py +116 -0
  40. pathsim/solvers/rkf45.py +102 -0
  41. pathsim/solvers/rkf78.py +111 -0
  42. pathsim/solvers/rkv65.py +103 -0
  43. pathsim/solvers/ssprk22.py +62 -0
  44. pathsim/solvers/ssprk33.py +65 -0
  45. pathsim/solvers/ssprk34.py +74 -0
  46. pathsim/subsystem.py +267 -0
  47. pathsim/utils/__init__.py +0 -0
  48. pathsim/utils/adaptivebuffer.py +87 -0
  49. pathsim/utils/anderson.py +180 -0
  50. pathsim/utils/funcs.py +205 -0
  51. pathsim/utils/gilbert.py +110 -0
  52. pathsim/utils/progresstracker.py +90 -0
  53. pathsim/utils/realtimeplotter.py +230 -0
  54. pathsim/utils/statespacerealizations.py +116 -0
  55. pathsim/utils/waveforms.py +36 -0
  56. pathsim-0.2.0.dist-info/LICENSE.txt +21 -0
  57. pathsim-0.2.0.dist-info/METADATA +149 -0
  58. pathsim-0.2.0.dist-info/RECORD +109 -0
  59. pathsim-0.2.0.dist-info/WHEEL +5 -0
  60. pathsim-0.2.0.dist-info/top_level.txt +2 -0
  61. tests/__init__.py +0 -0
  62. tests/blocks/__init__.py +0 -0
  63. tests/blocks/test_adder.py +85 -0
  64. tests/blocks/test_amplifier.py +66 -0
  65. tests/blocks/test_block.py +138 -0
  66. tests/blocks/test_delay.py +122 -0
  67. tests/blocks/test_differentiator.py +102 -0
  68. tests/blocks/test_function.py +165 -0
  69. tests/blocks/test_integrator.py +92 -0
  70. tests/blocks/test_lti.py +162 -0
  71. tests/blocks/test_multiplier.py +87 -0
  72. tests/blocks/test_ode.py +125 -0
  73. tests/blocks/test_rng.py +109 -0
  74. tests/blocks/test_scope.py +196 -0
  75. tests/blocks/test_sources.py +119 -0
  76. tests/blocks/test_spectrum.py +119 -0
  77. tests/solvers/__init__.py +0 -0
  78. tests/solvers/test_bdf.py +364 -0
  79. tests/solvers/test_dirk2.py +138 -0
  80. tests/solvers/test_dirk3.py +137 -0
  81. tests/solvers/test_esdirk32.py +158 -0
  82. tests/solvers/test_esdirk4.py +138 -0
  83. tests/solvers/test_esdirk43.py +158 -0
  84. tests/solvers/test_esdirk54.py +160 -0
  85. tests/solvers/test_esdirk85.py +157 -0
  86. tests/solvers/test_euler.py +223 -0
  87. tests/solvers/test_rk4.py +138 -0
  88. tests/solvers/test_rkbs32.py +159 -0
  89. tests/solvers/test_rkck54.py +157 -0
  90. tests/solvers/test_rkdp54.py +159 -0
  91. tests/solvers/test_rkdp87.py +157 -0
  92. tests/solvers/test_rkf45.py +159 -0
  93. tests/solvers/test_rkf78.py +160 -0
  94. tests/solvers/test_rkv65.py +160 -0
  95. tests/solvers/test_solver.py +119 -0
  96. tests/solvers/test_ssprk22.py +136 -0
  97. tests/solvers/test_ssprk33.py +136 -0
  98. tests/solvers/test_ssprk34.py +136 -0
  99. tests/test_connection.py +176 -0
  100. tests/test_simulation.py +271 -0
  101. tests/test_subsystem.py +182 -0
  102. tests/utils/__init__.py +0 -0
  103. tests/utils/test_adaptivebuffer.py +111 -0
  104. tests/utils/test_anderson.py +142 -0
  105. tests/utils/test_funcs.py +143 -0
  106. tests/utils/test_gilbert.py +108 -0
  107. tests/utils/test_progresstracker.py +144 -0
  108. tests/utils/test_realtimeplotter.py +122 -0
  109. tests/utils/test_statespacerealizations.py +107 -0
@@ -0,0 +1,143 @@
1
+ ########################################################################################
2
+ ##
3
+ ## TESTS FOR 'utils/funcs.py'
4
+ ##
5
+ ## Milan Rother 2023/24
6
+ ##
7
+ ########################################################################################
8
+
9
+ # IMPORTS ==============================================================================
10
+
11
+ import unittest
12
+ import numpy as np
13
+
14
+ from pathsim.utils.funcs import (
15
+ dict_to_array,
16
+ array_to_dict,
17
+ abs_error,
18
+ rel_error,
19
+ max_error,
20
+ max_error_dicts,
21
+ max_rel_error,
22
+ max_rel_error_dicts,
23
+ numerical_jacobian,
24
+ auto_jacobian
25
+ )
26
+
27
+
28
+ # TESTS ================================================================================
29
+
30
+ class TestUtilsFuncs(unittest.TestCase):
31
+ """
32
+ test all the array-dict conversions and functions for error calculation
33
+ """
34
+
35
+ def test_abs_error(self):
36
+
37
+ self.assertEqual(abs_error(1.0, 2.0), 1.0)
38
+ self.assertEqual(abs_error(2.0, 1.0), 1.0)
39
+ self.assertEqual(abs_error(2.0, 0.0), 2.0)
40
+ self.assertEqual(abs_error(0.0, 1.0), 1.0)
41
+
42
+
43
+ def test_rel_error(self):
44
+
45
+ #test fallback to abs error
46
+ self.assertEqual(rel_error(0.0, 1), 1)
47
+ self.assertEqual(rel_error(0.0, 0.1), 0.1)
48
+
49
+ #test a<b
50
+ self.assertEqual(rel_error(1.0, 2.0), 1.0)
51
+
52
+ #test a>b
53
+ self.assertEqual(rel_error(2.0, 1.0), 0.5)
54
+
55
+
56
+ def test_max_error(self):
57
+ self.assertEqual(max_error([0.00139, 2.4, 87, 1, 97.8, 4.35],
58
+ [1.00139, 1.4, 86, 2, 98.2, 33.35]),
59
+ 29)
60
+
61
+
62
+ def test_max_rel_error(self):
63
+ self.assertEqual(max_rel_error([0.00139, 2.4, 87, 1, 97.8, 4.35],
64
+ [1.00139, 1.4, 86, 2, 98.2, 33.35]),
65
+ 1/0.00139)
66
+
67
+
68
+ def test_dict_to_array(self):
69
+
70
+ #test conversion
71
+ self.assertEqual(np.sum(np.abs(dict_to_array({0:12, 1:3.2, 2:31.0})-np.array([12, 3.2, 31.0]))), 0.0)
72
+
73
+ #test key sorting
74
+ self.assertEqual(np.sum(np.abs(dict_to_array({0:12, 2:3.2, 1:31.0}) - np.array([12, 31.0, 3.2]))), 0.0)
75
+ self.assertEqual(np.sum(np.abs(dict_to_array({1:12, 2:3.2, 3:31.0})- np.array([12, 3.2, 31.0]))), 0.0)
76
+
77
+ #test non uniform keys
78
+ self.assertEqual(np.sum(np.abs(dict_to_array({0:12, 2:3.2, 3:31.0}) - np.array([12, 3.2, 31.0]))), 0.0)
79
+
80
+
81
+ def test_array_to_dict(self):
82
+
83
+ #test scalar input
84
+ self.assertEqual(array_to_dict(4), {0:4})
85
+
86
+ #test array input
87
+ self.assertEqual(array_to_dict(np.array([12, 3.2, 31.0])), {0:12, 1:3.2, 2:31.0})
88
+ self.assertEqual(array_to_dict(np.array([2.0])), {0:2.0})
89
+
90
+
91
+ def test_max_rel_error_dicts(self):
92
+ self.assertEqual(max_rel_error_dicts({0:0.00139, 1:2.4, 2:87, 3:1, 4:97.8, 5:4.35},
93
+ {0:1.00139, 1:1.4, 2:86, 3:2, 4:98.2, 5:33.35}),
94
+ 1/0.00139)
95
+
96
+ def test_max_error_dicts(self):
97
+ self.assertEqual(max_error_dicts({0:0.00139, 1:2.4, 2:87, 3:1, 4:97.8, 5:4.35},
98
+ {0:1.00139, 1:1.4, 2:86, 3:2, 4:98.2, 5:33.35}),
99
+ 29)
100
+
101
+
102
+ class TestJacobian(unittest.TestCase):
103
+ """
104
+ testing of numerical jacobian calculation
105
+ """
106
+
107
+ def test_numerical_jacobian(self):
108
+
109
+ #test scalar case
110
+ def _f(x): return x**2
111
+ def _df(x): return 2*x
112
+ self.assertAlmostEqual(numerical_jacobian(_f, 3), _df(3), 4)
113
+ self.assertAlmostEqual(numerical_jacobian(_f, -6.0), _df(-6.0), 4)
114
+ self.assertAlmostEqual(numerical_jacobian(_f, 100), _df(100), 3)
115
+
116
+ #test vectorial case
117
+ def _f(x): return np.array([np.cos(x[0]), np.sin(x[1])])
118
+ def _df(x): return np.array([[-np.sin(x[0]), 0.0], [0.0, np.cos(x[1])]])
119
+ self.assertAlmostEqual(np.sum(np.abs(numerical_jacobian(_f, np.ones(2)) - _df(np.ones(2)))), 0.0, 6)
120
+
121
+
122
+ def test_auto_jacobian(self):
123
+
124
+ #test scalar case
125
+ def _f(x): return x**2
126
+ def _df(x): return 2*x
127
+ _j = auto_jacobian(_f)
128
+ self.assertAlmostEqual(_j(3), _df(3), 6)
129
+ self.assertAlmostEqual(_j(-6.0), _df(-6.0), 6)
130
+ self.assertAlmostEqual(_j(100), _df(100), 3)
131
+
132
+ #test vectorial case
133
+ def _f(x): return np.array([np.cos(x[0]), np.sin(x[1])])
134
+ def _df(x): return np.array([[-np.sin(x[0]), 0.0], [0.0, np.cos(x[1])]])
135
+ _j = auto_jacobian(_f)
136
+ self.assertAlmostEqual(np.sum(np.abs(_j(np.ones(2)) - _df(np.ones(2)))), 0.0, 6)
137
+
138
+
139
+
140
+ # RUN TESTS LOCALLY ====================================================================
141
+
142
+ if __name__ == '__main__':
143
+ unittest.main(verbosity=2)
@@ -0,0 +1,108 @@
1
+ ########################################################################################
2
+ ##
3
+ ## TESTS FOR
4
+ ## 'utils/gilbert.py'
5
+ ##
6
+ ## Milan Rother 2024
7
+ ##
8
+ ########################################################################################
9
+
10
+ # IMPORTS ==============================================================================
11
+
12
+ import unittest
13
+ import numpy as np
14
+ from pathsim.utils.gilbert import gilbert_realization
15
+
16
+
17
+ # HELPER FUNCTIONS =====================================================================
18
+
19
+ def evaluate_statespace(s, A, B, C, D):
20
+ n = A.shape[0]
21
+ return np.dot(C, np.linalg.solve(s*np.eye(n) - A, B)) + D
22
+
23
+ def evaluate_poleresidue(s, Poles, Residues, Const):
24
+ return np.sum([r/(s - p) for p, r in zip(Poles, Residues)], axis=0) + Const
25
+
26
+
27
+ # TESTS ================================================================================
28
+
29
+ class TestGilbertRealization(unittest.TestCase):
30
+ """
31
+ Test the implementation of the gilbert statespace realization
32
+ """
33
+
34
+ def assertArrayAlmostEqual(self, first, second, places=7, msg=None):
35
+ np.testing.assert_array_almost_equal(first, second, decimal=places, err_msg=msg)
36
+
37
+ def test_helpers(self):
38
+ A, B, C, D = np.array([[1]]), np.array([[1]]), np.array([[1]]), np.array([[1]])
39
+ self.assertArrayAlmostEqual(evaluate_statespace(0, A, B, C, D), np.array([[0]]))
40
+ self.assertArrayAlmostEqual(evaluate_statespace(1j, A, B, C, D), np.array([[1+1/(1j-1)]]))
41
+
42
+ Poles, Residues, Const = [1], [np.array([1])], np.array([[1]])
43
+ self.assertArrayAlmostEqual(evaluate_poleresidue(0, Poles, Residues, Const), np.array([[0]]))
44
+ self.assertArrayAlmostEqual(evaluate_poleresidue(1j, Poles, Residues, Const), np.array([[1+1/(1j-1)]]))
45
+
46
+ def test_siso_real_poles(self):
47
+ Poles = [-1.0, -2.0, -3.0]
48
+ Residues = [1.0, 2.0, 3.0]
49
+ Const = 0.5
50
+ A, B, C, D = gilbert_realization(Poles, Residues, Const)
51
+
52
+ for s in [0, 1j, 10j, 100j]:
53
+ ss_eval = evaluate_statespace(s, A, B, C, D)
54
+ pr_eval = evaluate_poleresidue(s, Poles, Residues, Const)
55
+ self.assertArrayAlmostEqual(ss_eval, pr_eval, places=12)
56
+
57
+ def test_siso_complex_poles(self):
58
+ Poles = [-1+1j, -1-1j, -2]
59
+ Residues = [1-0.5j, 1+0.5j, 2]
60
+ Const = 0.1
61
+ A, B, C, D = gilbert_realization(Poles, Residues, Const)
62
+
63
+ for s in [0, 1j, 10j, 100j]:
64
+ ss_eval = evaluate_statespace(s, A, B, C, D)
65
+ pr_eval = evaluate_poleresidue(s, Poles, Residues, Const)
66
+ self.assertArrayAlmostEqual(ss_eval, pr_eval, places=12)
67
+
68
+ def test_mimo_2x2(self):
69
+ Poles = [-1, -2, -3]
70
+ Residues = [np.array([[1, 2], [3, 4]]),
71
+ np.array([[2, 3], [4, 5]]),
72
+ np.array([[3, 4], [5, 6]])]
73
+ Const = np.array([[0.1, 0.2], [0.3, 0.4]])
74
+ A, B, C, D = gilbert_realization(Poles, Residues, Const)
75
+
76
+ for s in [0, 1j, 10j, 100j]:
77
+ ss_eval = evaluate_statespace(s, A, B, C, D)
78
+ pr_eval = evaluate_poleresidue(s, Poles, Residues, Const)
79
+ self.assertArrayAlmostEqual(ss_eval, pr_eval, places=12)
80
+
81
+ def test_mimo_3x2(self):
82
+ Poles = [-1+1j, -1-1j, -2]
83
+ Residues = [np.array([[1, 2], [3, 4], [5, 6]]),
84
+ np.array([[1, 2], [3, 4], [5, 6]]),
85
+ np.array([[2, 3], [4, 5], [6, 7]])]
86
+ Const = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
87
+ A, B, C, D = gilbert_realization(Poles, Residues, Const)
88
+
89
+ for s in [0, 1j, 10j, 100j]:
90
+ ss_eval = evaluate_statespace(s, A, B, C, D)
91
+ pr_eval = evaluate_poleresidue(s, Poles, Residues, Const)
92
+ self.assertArrayAlmostEqual(ss_eval, pr_eval, places=12)
93
+
94
+ def test_input_validation(self):
95
+
96
+ # Empty poles and residues
97
+ with self.assertRaises(ValueError):
98
+ gilbert_realization([], [], 0)
99
+
100
+ # Mismatched poles and residues
101
+ with self.assertRaises(ValueError):
102
+ gilbert_realization([1, 2], [np.array([1])], 0)
103
+
104
+
105
+ # RUN TESTS LOCALLY ====================================================================
106
+
107
+ if __name__ == '__main__':
108
+ unittest.main(verbosity=2)
@@ -0,0 +1,144 @@
1
+ ########################################################################################
2
+ ##
3
+ ## TESTS FOR
4
+ ## 'utils/progresstracker.py'
5
+ ##
6
+ ## Milan Rother 2023/24
7
+ ##
8
+ ########################################################################################
9
+
10
+ # IMPORTS ==============================================================================
11
+
12
+ import unittest
13
+ import numpy as np
14
+
15
+ from pathsim.utils.progresstracker import (
16
+ ProgressTracker
17
+ )
18
+
19
+
20
+ # TESTS ================================================================================
21
+
22
+ class TestProgressTracker(unittest.TestCase):
23
+ """
24
+ test the implementation of the 'ProgressTracker' class
25
+ """
26
+
27
+
28
+ def test_iter_successful_5(self):
29
+
30
+ #tracker with log interval 5/100 %
31
+ tracker = ProgressTracker(log_interval=5)
32
+
33
+ #test if display percentages are correctly computed
34
+ self.assertEqual(tracker.display_percentages,
35
+ [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100])
36
+
37
+ i, n = 0, 100
38
+
39
+ #iterate the tracker
40
+ for _ in tracker:
41
+ i += 1
42
+
43
+ #test tracker iteration condition
44
+ self.assertTrue(tracker.condition)
45
+
46
+ #check progress
47
+ tracker.check(progress=i/n, success=True)
48
+
49
+ #test tracker steps
50
+ self.assertEqual(tracker.steps, i)
51
+
52
+ #check successful steps tracker
53
+ self.assertEqual(tracker.successful_steps, i)
54
+
55
+
56
+ def test_iter_successful_10(self):
57
+
58
+ #tracker with log interval 10/100 %
59
+ tracker = ProgressTracker(log_interval=10)
60
+
61
+ #test if display percentages are correctly computed
62
+ self.assertEqual(tracker.display_percentages,
63
+ [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100])
64
+
65
+ i, n = 0, 100
66
+
67
+ #iterate the tracker
68
+ for _ in tracker:
69
+ i += 1
70
+
71
+ #test tracker iteration condition
72
+ self.assertTrue(tracker.condition)
73
+
74
+ #check progress
75
+ tracker.check(progress=i/n, success=True)
76
+
77
+ #test tracker steps
78
+ self.assertEqual(tracker.steps, i)
79
+
80
+ #check successful steps tracker
81
+ self.assertEqual(tracker.successful_steps, i)
82
+
83
+
84
+ def test_iter_mixed_success_5(self):
85
+
86
+ #tracker with log interval 5/100 %
87
+ tracker = ProgressTracker(log_interval=5)
88
+
89
+ #test if display percentages are correctly computed
90
+ self.assertEqual(tracker.display_percentages,
91
+ [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100])
92
+
93
+ i, j, n = 0, 50, 100
94
+
95
+ #iterate the tracker
96
+ for _ in tracker:
97
+ i += 1
98
+
99
+ #test tracker iteration condition
100
+ self.assertTrue(tracker.condition)
101
+
102
+ #check progress
103
+ tracker.check(progress=i/n, success=i>j)
104
+
105
+ #test tracker steps
106
+ self.assertEqual(tracker.steps, i)
107
+
108
+ #check successful steps tracker
109
+ self.assertEqual(tracker.successful_steps, max(0, i-j))
110
+
111
+
112
+ def test_iter_mixed_success_10(self):
113
+
114
+ #tracker with log interval 10/100 %
115
+ tracker = ProgressTracker(log_interval=10)
116
+
117
+ #test if display percentages are correctly computed
118
+ self.assertEqual(tracker.display_percentages,
119
+ [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100])
120
+
121
+ i, j, n = 0, 50, 100
122
+
123
+ #iterate the tracker
124
+ for _ in tracker:
125
+ i += 1
126
+
127
+ #test tracker iteration condition
128
+ self.assertTrue(tracker.condition)
129
+
130
+ #check progress
131
+ tracker.check(progress=i/n, success=i>j)
132
+
133
+ #test tracker steps
134
+ self.assertEqual(tracker.steps, i)
135
+
136
+ #check successful steps tracker
137
+ self.assertEqual(tracker.successful_steps, max(0, i-j))
138
+
139
+
140
+
141
+ # RUN TESTS LOCALLY ====================================================================
142
+
143
+ if __name__ == '__main__':
144
+ unittest.main(verbosity=2)
@@ -0,0 +1,122 @@
1
+ ########################################################################################
2
+ ##
3
+ ## TESTS FOR
4
+ ## 'utils/realtimeplotter.py'
5
+ ##
6
+ ## Milan Rother 2024
7
+ ##
8
+ ########################################################################################
9
+
10
+ # IMPORTS ==============================================================================
11
+
12
+ import unittest
13
+ from unittest.mock import patch, MagicMock
14
+ import numpy as np
15
+
16
+ from pathsim.utils.realtimeplotter import RealtimePlotter
17
+
18
+
19
+ # TESTS ================================================================================
20
+
21
+ class TestRealtimePlotter(unittest.TestCase):
22
+ """
23
+ test the implementation of the 'RealtimePlotter' class
24
+ """
25
+
26
+ def setUp(self):
27
+ self.max_samples = 100
28
+ self.update_interval = 0.5
29
+ self.labels = ['Test 1', 'Test 2']
30
+ self.x_label = 'X Axis'
31
+ self.y_label = 'Y Axis'
32
+
33
+ @patch('pathsim.utils.realtimeplotter.plt')
34
+ def test_init(self, mock_plt):
35
+ mock_fig = MagicMock()
36
+ mock_ax = MagicMock()
37
+ mock_plt.subplots.return_value = (mock_fig, mock_ax)
38
+
39
+ plotter = RealtimePlotter(
40
+ max_samples=self.max_samples,
41
+ update_interval=self.update_interval,
42
+ labels=self.labels,
43
+ x_label=self.x_label,
44
+ y_label=self.y_label
45
+ )
46
+
47
+ self.assertEqual(plotter.max_samples, self.max_samples)
48
+ self.assertEqual(plotter.update_interval, self.update_interval)
49
+ self.assertEqual(plotter.labels, self.labels)
50
+ self.assertEqual(plotter.x_label, self.x_label)
51
+ self.assertEqual(plotter.y_label, self.y_label)
52
+
53
+ mock_plt.subplots.assert_called_once()
54
+ mock_ax.set_xlabel.assert_called_once_with(self.x_label)
55
+ mock_ax.set_ylabel.assert_called_once_with(self.y_label)
56
+ mock_ax.grid.assert_called_once_with(True)
57
+
58
+ @patch('pathsim.utils.realtimeplotter.plt')
59
+ @patch('pathsim.utils.realtimeplotter.time')
60
+ def test_update_all(self, mock_time, mock_plt):
61
+ mock_fig = MagicMock()
62
+ mock_ax = MagicMock()
63
+ mock_line = MagicMock()
64
+ mock_ax.plot.return_value = [mock_line]
65
+ mock_plt.subplots.return_value = (mock_fig, mock_ax)
66
+ mock_time.time.return_value = 0
67
+
68
+ plotter = RealtimePlotter(labels=self.labels)
69
+
70
+ x = np.array([1, 2, 3])
71
+ y = np.array([[1, 2, 3], [4, 5, 6]])
72
+
73
+ result = plotter.update_all(x, y)
74
+
75
+ self.assertTrue(result)
76
+ self.assertTrue(hasattr(plotter, 'data'))
77
+ self.assertTrue(hasattr(plotter, 'lines'))
78
+
79
+ @patch('pathsim.utils.realtimeplotter.plt')
80
+ @patch('pathsim.utils.realtimeplotter.time')
81
+ def test_update(self, mock_time, mock_plt):
82
+ mock_fig = MagicMock()
83
+ mock_ax = MagicMock()
84
+ mock_line = MagicMock()
85
+ mock_ax.plot.return_value = [mock_line]
86
+ mock_plt.subplots.return_value = (mock_fig, mock_ax)
87
+ mock_time.time.return_value = 0
88
+
89
+ plotter = RealtimePlotter(max_samples=5)
90
+
91
+ for i in range(10):
92
+ result = plotter.update(i, i)
93
+ self.assertTrue(result)
94
+
95
+ self.assertTrue(hasattr(plotter, 'data'))
96
+ self.assertTrue(hasattr(plotter, 'lines'))
97
+
98
+ @patch('pathsim.utils.realtimeplotter.plt')
99
+ def test_on_close(self, mock_plt):
100
+ mock_fig = MagicMock()
101
+ mock_ax = MagicMock()
102
+ mock_plt.subplots.return_value = (mock_fig, mock_ax)
103
+
104
+ plotter = RealtimePlotter()
105
+ plotter.on_close(None)
106
+ self.assertFalse(plotter.is_running)
107
+
108
+ @patch('pathsim.utils.realtimeplotter.plt')
109
+ def test_show(self, mock_plt):
110
+ mock_fig = MagicMock()
111
+ mock_ax = MagicMock()
112
+ mock_plt.subplots.return_value = (mock_fig, mock_ax)
113
+
114
+ plotter = RealtimePlotter()
115
+ plotter.show()
116
+ mock_plt.show.assert_called_with(block=False)
117
+
118
+
119
+ # RUN TESTS LOCALLY ====================================================================
120
+
121
+ if __name__ == '__main__':
122
+ unittest.main(verbosity=2)
@@ -0,0 +1,107 @@
1
+ ########################################################################################
2
+ ##
3
+ ## TESTS FOR 'utils/statespacerealizations.py'
4
+ ##
5
+ ## Milan Rother 2024
6
+ ##
7
+ ########################################################################################
8
+
9
+ # IMPORTS ==============================================================================
10
+
11
+ import unittest
12
+ import numpy as np
13
+ from pathsim.utils.statespacerealizations import gilbert_realization
14
+
15
+
16
+ # HELPER FUNCTIONS =====================================================================
17
+
18
+ def evaluate_statespace(s, A, B, C, D):
19
+ n = A.shape[0]
20
+ return np.dot(C, np.linalg.solve(s*np.eye(n) - A, B)) + D
21
+
22
+ def evaluate_poleresidue(s, Poles, Residues, Const):
23
+ return np.sum([r/(s - p) for p, r in zip(Poles, Residues)], axis=0) + Const
24
+
25
+
26
+ # TESTS ================================================================================
27
+
28
+ class GilbertRealizationTest(unittest.TestCase):
29
+ """
30
+ Test the implementation of the gilbert statespace realization
31
+ """
32
+
33
+ def assertArrayAlmostEqual(self, first, second, places=7, msg=None):
34
+ np.testing.assert_array_almost_equal(first, second, decimal=places, err_msg=msg)
35
+
36
+ def test_helpers(self):
37
+ A, B, C, D = np.array([[1]]), np.array([[1]]), np.array([[1]]), np.array([[1]])
38
+ self.assertArrayAlmostEqual(evaluate_statespace(0, A, B, C, D), np.array([[0]]))
39
+ self.assertArrayAlmostEqual(evaluate_statespace(1j, A, B, C, D), np.array([[1+1/(1j-1)]]))
40
+
41
+ Poles, Residues, Const = [1], [np.array([1])], np.array([[1]])
42
+ self.assertArrayAlmostEqual(evaluate_poleresidue(0, Poles, Residues, Const), np.array([[0]]))
43
+ self.assertArrayAlmostEqual(evaluate_poleresidue(1j, Poles, Residues, Const), np.array([[1+1/(1j-1)]]))
44
+
45
+ def test_siso_real_poles(self):
46
+ Poles = [-1.0, -2.0, -3.0]
47
+ Residues = [1.0, 2.0, 3.0]
48
+ Const = 0.5
49
+ A, B, C, D = gilbert_realization(Poles, Residues, Const)
50
+
51
+ for s in [0, 1j, 10j, 100j]:
52
+ ss_eval = evaluate_statespace(s, A, B, C, D)
53
+ pr_eval = evaluate_poleresidue(s, Poles, Residues, Const)
54
+ self.assertArrayAlmostEqual(ss_eval, pr_eval, places=12)
55
+
56
+ def test_siso_complex_poles(self):
57
+ Poles = [-1+1j, -1-1j, -2]
58
+ Residues = [1-0.5j, 1+0.5j, 2]
59
+ Const = 0.1
60
+ A, B, C, D = gilbert_realization(Poles, Residues, Const)
61
+
62
+ for s in [0, 1j, 10j, 100j]:
63
+ ss_eval = evaluate_statespace(s, A, B, C, D)
64
+ pr_eval = evaluate_poleresidue(s, Poles, Residues, Const)
65
+ self.assertArrayAlmostEqual(ss_eval, pr_eval, places=12)
66
+
67
+ def test_mimo_2x2(self):
68
+ Poles = [-1, -2, -3]
69
+ Residues = [np.array([[1, 2], [3, 4]]),
70
+ np.array([[2, 3], [4, 5]]),
71
+ np.array([[3, 4], [5, 6]])]
72
+ Const = np.array([[0.1, 0.2], [0.3, 0.4]])
73
+ A, B, C, D = gilbert_realization(Poles, Residues, Const)
74
+
75
+ for s in [0, 1j, 10j, 100j]:
76
+ ss_eval = evaluate_statespace(s, A, B, C, D)
77
+ pr_eval = evaluate_poleresidue(s, Poles, Residues, Const)
78
+ self.assertArrayAlmostEqual(ss_eval, pr_eval, places=12)
79
+
80
+ def test_mimo_3x2(self):
81
+ Poles = [-1+1j, -1-1j, -2]
82
+ Residues = [np.array([[1, 2], [3, 4], [5, 6]]),
83
+ np.array([[1, 2], [3, 4], [5, 6]]),
84
+ np.array([[2, 3], [4, 5], [6, 7]])]
85
+ Const = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
86
+ A, B, C, D = gilbert_realization(Poles, Residues, Const)
87
+
88
+ for s in [0, 1j, 10j, 100j]:
89
+ ss_eval = evaluate_statespace(s, A, B, C, D)
90
+ pr_eval = evaluate_poleresidue(s, Poles, Residues, Const)
91
+ self.assertArrayAlmostEqual(ss_eval, pr_eval, places=12)
92
+
93
+ def test_input_validation(self):
94
+
95
+ # Empty poles and residues
96
+ with self.assertRaises(ValueError):
97
+ gilbert_realization([], [], 0)
98
+
99
+ # Mismatched poles and residues
100
+ with self.assertRaises(ValueError):
101
+ gilbert_realization([1, 2], [np.array([1])], 0)
102
+
103
+
104
+ # RUN TESTS LOCALLY ====================================================================
105
+
106
+ if __name__ == '__main__':
107
+ unittest.main(verbosity=2)