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
« 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/>.
20import collections 1bca
22import numpy as np 1bca
25class Deriv: # noqa: PLW1641, unhashable 1bca
26 """
27 Class for specifying derivatives.
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.
33 Deriv(int) -> specified order derivative
35 Deriv(str) -> first derivative w.r.t. specified variable
37 Deriv(iter of str) -> derivative w.r.t. specified variables
39 Deriv(iter of int, str) -> an int before a str acts as a multiplier
41 Deriv(Deriv) -> pass through
43 Deriv(None) -> Deriv(0)
45 Example: Deriv(['a', 'b', 'b', 'c']) is equivalent to
46 Deriv(['a', 2, 'b', 'c']).
48 Attributes
49 ----------
50 implicit
51 order
52 max
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 """
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
109 def __getitem__(self, key): 1bca
110 return self._counter[key] 1bca
112 def __iter__(self): 1bca
113 return iter(self._counter) 1bca
115 def __len__(self): 1bca
116 return len(self._counter) 1a
118 def __bool__(self): 1bca
119 return bool(self._counter) 1bca
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
127 def __repr__(self): 1bca
128 return dict.__repr__(self._counter) 1a
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
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
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)