Coverage for src/lsqfitgp/_Deriv.py: 97%

69 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-10-04 11:43 +0000

1# lsqfitgp/_Deriv.py 

2# 

3# Copyright (c) 2020, 2022, 2024, 2026, Giacomo Petrillo 

4# 

5# This file is part of lsqfitgp. 

6# 

7# lsqfitgp is free software: you can redistribute it and/or modify 

8# it under the terms of the GNU General Public License as published by 

9# the Free Software Foundation, either version 3 of the License, or 

10# (at your option) any later version. 

11# 

12# lsqfitgp is distributed in the hope that it will be useful, 

13# but WITHOUT ANY WARRANTY; without even the implied warranty of 

14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

15# GNU General Public License for more details. 

16# 

17# You should have received a copy of the GNU General Public License 

18# along with lsqfitgp. If not, see <http://www.gnu.org/licenses/>. 

19 

20import collections 1bca

21 

22import numpy as np 1bca

23 

24 

25class Deriv: # noqa: PLW1641, unhashable 1bca

26 """ 

27 Class for specifying derivatives. 

28 

29 Behaves like a dictionary str -> int, where the keys represent variables 

30 and values the derivation order. An empty Deriv means no derivatives. A 

31 Deriv with one single key None means that the variable is implicit. 

32 

33 Deriv(int) -> specified order derivative 

34 

35 Deriv(str) -> first derivative w.r.t. specified variable 

36 

37 Deriv(iter of str) -> derivative w.r.t. specified variables 

38 

39 Deriv(iter of int, str) -> an int before a str acts as a multiplier 

40 

41 Deriv(Deriv) -> pass through 

42 

43 Deriv(None) -> Deriv(0) 

44 

45 Example: Deriv(['a', 'b', 'b', 'c']) is equivalent to 

46 Deriv(['a', 2, 'b', 'c']). 

47 

48 Attributes 

49 ---------- 

50 implicit 

51 order 

52 max 

53 

54 Raises 

55 ------ 

56 TypeError 

57 If ``*args`` is not of the specified form. 

58 ValueError 

59 If ``*args`` ends with an integer or if there are consecutive 

60 integers. 

61 """ 

62 

63 def __new__(cls, *args): # noqa: C901 1bca

64 c = collections.Counter() 1bca

65 if len(args) == 1: 1bca

66 arg = args[0] 1bca

67 if isinstance(arg, cls): 1bca

68 return arg 1bca

69 elif isinstance(arg, (int, np.integer)): 1bca

70 if arg < 0: 1bca

71 msg = f'degree {arg} is negative' 1a

72 raise ValueError(msg) 1a

73 if arg: 1bca

74 c.update({None: arg}) 1bca

75 elif isinstance(arg, str): 1bca

76 c.update([arg]) 1bca

77 elif np.iterable(arg): 1bca

78 integer = None 1bca

79 for obj in arg: 1bca

80 if isinstance(obj, str): 1bca

81 if integer is not None: 1bca

82 if integer: 1bca

83 c.update({obj: integer}) 1bca

84 integer = None 1bca

85 else: 

86 c.update([obj]) 1ba

87 elif isinstance(obj, (int, np.integer)): 1bca

88 assert obj >= 0, obj 1bca

89 if integer is not None: 1bca

90 msg = 'consecutive integers in iterable' 1a

91 raise ValueError(msg) 1a

92 integer = int(obj) 1bca

93 else: 

94 msg = 'objects in iterable must be int or str' 1a

95 raise TypeError(msg) 1a

96 if integer is not None: 1bca

97 msg = 'dangling derivative order' 1a

98 raise ValueError(msg) 1a

99 elif arg is not None: 1bca

100 msg = 'argument must be None, int, str, or iterable' 1a

101 raise TypeError(msg) 1a

102 elif len(args) != 0: 1a

103 raise ValueError(len(args)) 1a

104 assert all(c.values()) 1bca

105 self = super().__new__(cls) 1bca

106 self._counter = c 1bca

107 return self 1bca

108 

109 def __getitem__(self, key): 1bca

110 return self._counter[key] 1bca

111 

112 def __iter__(self): 1bca

113 return iter(self._counter) 1bca

114 

115 def __len__(self): 1bca

116 return len(self._counter) 1a

117 

118 def __bool__(self): 1bca

119 return bool(self._counter) 1bca

120 

121 def __eq__(self, val): 1bca

122 if isinstance(val, Deriv): 122 ↛ 123line 122 didn't jump to line 123 because the condition on line 122 was never true1a

123 return self._counter == val._counter 

124 else: 

125 return NotImplemented 1a

126 

127 def __repr__(self): 1bca

128 return dict.__repr__(self._counter) 1a

129 

130 @property 1bca

131 def implicit(self): 1bca

132 """True if the derivative is trivial or the variable is implicit.""" 

133 return not self or next(iter(self._counter)) is None 1bca

134 

135 @property 1bca

136 def order(self): 1bca

137 """The total derivation order, i.e., the sum of the values.""" 

138 # return self._counter.total() # works only in Python >=3.10 

139 return sum(self._counter.values()) 1bca

140 

141 @property 1bca

142 def max(self): 1bca

143 """The maximum derivation order for any single variable.""" 

144 return max(self._counter.values(), default=0)