Coverage for pyrc \ tests \ test_symbolic.py: 100%

77 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-13 16:59 +0200

1# ------------------------------------------------------------------------------- 

2# Copyright (C) 2026 Joel Kimmich, Tim Jourdan 

3# ------------------------------------------------------------------------------ 

4# License 

5# This file is part of PyRC, distributed under GPL-3.0-or-later. 

6# ------------------------------------------------------------------------------ 

7 

8from unittest import TestCase 

9 

10import numpy as np 

11from sympy import SparseMatrix, Symbol, sin, cos 

12from pyrc.core.solver.symbolic import SparseSymbolicEvaluator 

13 

14t, w = Symbol("t"), Symbol("w") 

15 

16 

17class TestSparseSymbolicEvaluator(TestCase): 

18 

19 def test_constant_matrix(self): 

20 """Matrix with only constant entries evaluates correctly.""" 

21 M = SparseMatrix(3, 3, {(0, 0): 1.0, (1, 2): 3.0, (2, 1): -2.0}) 

22 ev = SparseSymbolicEvaluator(M, []) 

23 result = ev.evaluate() 

24 expected = np.array([[1, 0, 0], [0, 0, 3], [0, -2, 0]], dtype=float) 

25 np.testing.assert_allclose(result.toarray(), expected) 

26 

27 def test_symbolic_single_entry(self): 

28 """Single symbolic entry is evaluated and placed correctly alongside a constant.""" 

29 M = SparseMatrix(2, 2, {(0, 0): t, (1, 1): 2.0}) 

30 ev = SparseSymbolicEvaluator(M, [t]) 

31 result = ev.evaluate(np.array([3.0])) 

32 expected = np.array([[3.0, 0], [0, 2.0]]) 

33 np.testing.assert_allclose(result.toarray(), expected) 

34 

35 def test_symbolic_multiple_symbols(self): 

36 """Multiple symbols in multiple entries are evaluated correctly.""" 

37 M = SparseMatrix(2, 2, {(0, 0): t + w, (0, 1): w, (1, 0): t}) 

38 ev = SparseSymbolicEvaluator(M, [t, w]) 

39 result = ev.evaluate(np.array([1.0, 2.0])) 

40 expected = np.array([[3.0, 2.0], [1.0, 0.0]]) 

41 np.testing.assert_allclose(result.toarray(), expected) 

42 

43 def test_nonlinear_expression(self): 

44 """Nonlinear symbolic expressions (sin, cos) are evaluated correctly.""" 

45 M = SparseMatrix(2, 2, {(0, 0): sin(t), (1, 1): cos(t)}) 

46 ev = SparseSymbolicEvaluator(M, [t]) 

47 val = np.pi / 4 

48 result = ev.evaluate(np.array([val])) 

49 expected = np.array([[np.sin(val), 0], [0, np.cos(val)]]) 

50 np.testing.assert_allclose(result.toarray(), expected) 

51 

52 def test_mixed_constant_and_symbolic(self): 

53 """Matrix with mixed constant and symbolic entries evaluates correctly.""" 

54 M = SparseMatrix(2, 2, {(0, 0): t, (0, 1): 5.0, (1, 1): w}) 

55 ev = SparseSymbolicEvaluator(M, [t, w]) 

56 result = ev.evaluate(np.array([2.0, -1.0])) 

57 expected = np.array([[2.0, 5.0], [0.0, -1.0]]) 

58 np.testing.assert_allclose(result.toarray(), expected) 

59 

60 def test_working_matrix_not_modified_between_calls(self): 

61 """Consecutive calls with different values return correct results without state leakage.""" 

62 M = SparseMatrix(2, 2, {(0, 0): t}) 

63 ev = SparseSymbolicEvaluator(M, [t]) 

64 ev.evaluate(np.array([10.0])) 

65 result = ev.evaluate(np.array([3.0])) 

66 expected = np.array([[3.0, 0], [0, 0.0]]) 

67 np.testing.assert_allclose(result.toarray(), expected) 

68 

69 def test_empty_matrix(self): 

70 """Matrix with no nonzero entries returns a zero matrix.""" 

71 M = SparseMatrix(3, 3, {}) 

72 ev = SparseSymbolicEvaluator(M, [t]) 

73 result = ev.evaluate(np.array([1.0])) 

74 np.testing.assert_allclose(result.toarray(), np.zeros((3, 3))) 

75 

76 def test_shape(self): 

77 """Evaluator and output matrix have the correct shape.""" 

78 M = SparseMatrix(4, 6, {(0, 0): t, (3, 5): 1.0}) 

79 ev = SparseSymbolicEvaluator(M, [t]) 

80 self.assertEqual(ev.shape, (4, 6)) 

81 self.assertEqual(ev.evaluate(np.array([1.0])).shape, (4, 6)) 

82 

83 def test_large_sparse_matrix(self): 

84 """Random 1000x1000 sparse matrix with ~20 entries per row, mixed constant and symbolic, evaluates correctly.""" 

85 rng = np.random.default_rng(42) 

86 n = 1000 

87 entries_per_row = 20 

88 M_dict = {} 

89 expected = np.zeros((n, n)) 

90 

91 t_val, w_val = 2.5, -1.3 

92 

93 for i in range(n): 

94 cols = rng.choice(n, size=entries_per_row, replace=False) 

95 for j in cols: 

96 if rng.random() < 0.5: 

97 val = rng.uniform(-10, 10) 

98 M_dict[(i, int(j))] = val 

99 expected[i, int(j)] += val 

100 elif rng.random() < 0.5: 

101 M_dict[(i, int(j))] = t 

102 expected[i, int(j)] += t_val 

103 else: 

104 M_dict[(i, int(j))] = w 

105 expected[i, int(j)] += w_val 

106 

107 M = SparseMatrix(n, n, M_dict) 

108 ev = SparseSymbolicEvaluator(M, [t, w]) 

109 result = ev.evaluate(np.array([t_val, w_val])) 

110 np.testing.assert_allclose(result.toarray(), expected)