uncertainty-tools 0.0.1__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.
- unc_tools/__init__.py +26 -0
- unc_tools/default_functions.py +516 -0
- unc_tools/exceptions.py +31 -0
- unc_tools/patches.py +487 -0
- unc_tools/py.typed +1 -0
- unc_tools/unc_regression.py +600 -0
- uncertainty_tools-0.0.1.dist-info/METADATA +101 -0
- uncertainty_tools-0.0.1.dist-info/RECORD +10 -0
- uncertainty_tools-0.0.1.dist-info/WHEEL +4 -0
- uncertainty_tools-0.0.1.dist-info/licenses/LICENSE +21 -0
unc_tools/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Public package interface for uncertainty-aware tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
|
|
6
|
+
from .default_functions import FunctionBase1D, Hyper, Poly
|
|
7
|
+
from .exceptions import DataError, ExpressionError, InitialGuessError, ModelTypeError
|
|
8
|
+
from .unc_regression import UncRegression
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("uncertainty_tools")
|
|
13
|
+
except PackageNotFoundError:
|
|
14
|
+
__version__ = "unknown"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"DataError",
|
|
18
|
+
"ExpressionError",
|
|
19
|
+
"FunctionBase1D",
|
|
20
|
+
"Hyper",
|
|
21
|
+
"InitialGuessError",
|
|
22
|
+
"ModelTypeError",
|
|
23
|
+
"Poly",
|
|
24
|
+
"UncRegression",
|
|
25
|
+
"__version__",
|
|
26
|
+
]
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"""Symbolic helper utilities with uncertainty-aware coefficients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from typing import Callable, Sequence
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import re
|
|
10
|
+
import sympy as sym
|
|
11
|
+
import uncertainties as unc
|
|
12
|
+
|
|
13
|
+
from .exceptions import ExpressionError
|
|
14
|
+
|
|
15
|
+
__all__ = ["FunctionBase1D", "Hyper", "Poly"]
|
|
16
|
+
|
|
17
|
+
Uncertain = unc.core.AffineScalarFunc | unc.core.Variable
|
|
18
|
+
Numeric = float | int | np.number
|
|
19
|
+
Coefficient = sym.Expr | Numeric | Uncertain
|
|
20
|
+
CoefficientsInput = Sequence[Coefficient] | np.ndarray
|
|
21
|
+
Solution = sym.Expr | Uncertain | tuple[Uncertain, Uncertain]
|
|
22
|
+
LambdaReturn = float | complex | np.ndarray | Uncertain
|
|
23
|
+
InputValue = Numeric | np.ndarray | Uncertain
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FunctionBase1D:
|
|
27
|
+
"""Represent a one-dimensional symbolic expression with optional uncertainty data."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, expr_str: str = "") -> None:
|
|
30
|
+
"""Initialize the expression and extract coefficient symbols.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
expr_str: String representation of the sympy expression to manage.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
sym.SympifyError: If the expression string cannot be parsed.
|
|
37
|
+
"""
|
|
38
|
+
self.expr_str = expr_str
|
|
39
|
+
self.expr_unc: str = ""
|
|
40
|
+
self.expr_sym = sym.parse_expr(expr_str)
|
|
41
|
+
self.args = self.ordered_symbols(expr_str)
|
|
42
|
+
|
|
43
|
+
self._added_coefs = False
|
|
44
|
+
self._unc_coefs = False
|
|
45
|
+
self._complex = False
|
|
46
|
+
self._show_complex = False
|
|
47
|
+
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def ordered_symbols(expr_str: str) -> list[sym.Symbol]:
|
|
52
|
+
"""Return sympy symbols in the order they appear in the expression string.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
expr_str: Expression string to scan.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
A list of symbols ordered by their first appearance in the expression.
|
|
59
|
+
"""
|
|
60
|
+
expr = sym.sympify(expr_str)
|
|
61
|
+
|
|
62
|
+
symbols = expr.free_symbols
|
|
63
|
+
|
|
64
|
+
name_to_symbol = {str(s): s for s in symbols}
|
|
65
|
+
|
|
66
|
+
ordered = []
|
|
67
|
+
used = set("x")
|
|
68
|
+
|
|
69
|
+
i = 0
|
|
70
|
+
n = len(expr_str)
|
|
71
|
+
|
|
72
|
+
while i < n:
|
|
73
|
+
for name, symb in name_to_symbol.items():
|
|
74
|
+
if expr_str.startswith(name, i):
|
|
75
|
+
if name not in used:
|
|
76
|
+
ordered.append(symb)
|
|
77
|
+
used.add(name)
|
|
78
|
+
i += len(name)
|
|
79
|
+
break
|
|
80
|
+
else:
|
|
81
|
+
i += 1
|
|
82
|
+
|
|
83
|
+
return ordered
|
|
84
|
+
|
|
85
|
+
@cached_property
|
|
86
|
+
def coefs(self) -> list[Coefficient]:
|
|
87
|
+
"""Return coefficient placeholders or bound values.
|
|
88
|
+
|
|
89
|
+
Before coefficients are bound, this list contains sympy symbols in a stable
|
|
90
|
+
order. After calling `add_coefs`, it contains the supplied coefficient values.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A list of coefficient symbols or values.
|
|
94
|
+
"""
|
|
95
|
+
return self.args
|
|
96
|
+
|
|
97
|
+
@cached_property
|
|
98
|
+
def sols(self) -> list[Solution]:
|
|
99
|
+
"""Solve the expression set equal to zero.
|
|
100
|
+
|
|
101
|
+
Returns analytical solutions, optionally excluding complex values unless
|
|
102
|
+
complex display is enabled.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
A list of solutions for the equation ``expr_sym = 0``.
|
|
106
|
+
"""
|
|
107
|
+
# try:
|
|
108
|
+
# solution = sym.solvers.solve(self.expr_sym - 0, sym.Symbol("x"))
|
|
109
|
+
#
|
|
110
|
+
# # if not solution:
|
|
111
|
+
# # raise TypeError("Cant find analytical solution for given expression")
|
|
112
|
+
#
|
|
113
|
+
# if self._show_complex:
|
|
114
|
+
# return solution
|
|
115
|
+
# else:
|
|
116
|
+
# solution = [x for x in solution if not x.is_imaginary]
|
|
117
|
+
# return solution
|
|
118
|
+
# except:
|
|
119
|
+
# return []
|
|
120
|
+
#
|
|
121
|
+
solution = sym.solvers.solve(self.expr_sym - 0, sym.Symbol("x"))
|
|
122
|
+
|
|
123
|
+
# if not solution:
|
|
124
|
+
# raise TypeError("Cant find analytical solution for given expression")
|
|
125
|
+
|
|
126
|
+
if self._show_complex:
|
|
127
|
+
return solution
|
|
128
|
+
else:
|
|
129
|
+
solution = [x for x in solution if not x.is_imaginary]
|
|
130
|
+
return solution
|
|
131
|
+
|
|
132
|
+
def find_sols(
|
|
133
|
+
self, y: int | float | sym.Expr | str | Uncertain = 0
|
|
134
|
+
) -> list[Solution] | Solution:
|
|
135
|
+
"""Solve the expression for a specified target value.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
y: Target value for ``expr_sym = y``. Strings are parsed as sympy expressions.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
A list of solutions or a single solution when only one exists.
|
|
142
|
+
"""
|
|
143
|
+
if isinstance(y, (unc.core.Variable, unc.core.AffineScalarFunc)):
|
|
144
|
+
new_expr = FunctionBase1D(self.expr_str + " - NEW_1")
|
|
145
|
+
|
|
146
|
+
if isinstance(self.coefs, np.ndarray):
|
|
147
|
+
self.coefs = self.coefs.tolist()
|
|
148
|
+
# print(type(self.coefs))
|
|
149
|
+
print(y)
|
|
150
|
+
list_coefs = self.coefs + [y]
|
|
151
|
+
# print(new_expr.coefs)
|
|
152
|
+
# print(list_coefs)
|
|
153
|
+
if self._added_coefs:
|
|
154
|
+
new_expr.add_coefs(list_coefs)
|
|
155
|
+
|
|
156
|
+
else:
|
|
157
|
+
if isinstance(y, str):
|
|
158
|
+
y = sym.parse_expr(y)
|
|
159
|
+
new_expr = FunctionBase1D(self.expr_str)
|
|
160
|
+
new_expr.expr_sym -= y
|
|
161
|
+
if self._added_coefs:
|
|
162
|
+
new_expr.add_coefs(self.coefs)
|
|
163
|
+
|
|
164
|
+
if len(new_expr.sols) > 1 and hasattr(new_expr.sols, "__iter__"):
|
|
165
|
+
return new_expr.sols
|
|
166
|
+
elif len(new_expr.sols) == 0:
|
|
167
|
+
return []
|
|
168
|
+
else:
|
|
169
|
+
return new_expr.sols[0]
|
|
170
|
+
|
|
171
|
+
@cached_property
|
|
172
|
+
def lambda_fun(self) -> Callable[..., LambdaReturn]:
|
|
173
|
+
"""Generate a numpy-evaluable callable for the expression.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
A callable accepting ``x`` followed by coefficient values.
|
|
177
|
+
"""
|
|
178
|
+
lambda_args = [sym.Symbol("x")] + self.args
|
|
179
|
+
return sym.lambdify(lambda_args, self.expr_sym, "numpy")
|
|
180
|
+
|
|
181
|
+
def show_complex(self, show: bool = False) -> "FunctionBase1D":
|
|
182
|
+
"""Toggle the display of complex solutions.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
show: Whether to include complex-valued solutions.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The current instance with updated display preference.
|
|
189
|
+
"""
|
|
190
|
+
self._show_complex = show
|
|
191
|
+
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _calculate_uncertainty_analyticaly(
|
|
196
|
+
expr: sym.Expr,
|
|
197
|
+
parms: Sequence[sym.Symbol] | None = None,
|
|
198
|
+
) -> sym.Expr:
|
|
199
|
+
"""Compute propagated uncertainty for a symbolic expression.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
expr: Expression whose uncertainty is estimated.
|
|
203
|
+
parms: Parameters treated as uncertain; defaults to free symbols.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
A symbolic expression representing the combined uncertainty.
|
|
207
|
+
"""
|
|
208
|
+
if parms is None:
|
|
209
|
+
parms = expr.free_symbols
|
|
210
|
+
|
|
211
|
+
unc_list: list[sym.Expr] = []
|
|
212
|
+
for parm in parms:
|
|
213
|
+
unc_list.append(
|
|
214
|
+
(sym.diff(expr, parm) * sym.Symbol(f"Delta_{str(parm)}")) ** 2
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return sym.sqrt(np.sum(unc_list))
|
|
218
|
+
|
|
219
|
+
def add_coefs(self, coefs: CoefficientsInput) -> "FunctionBase1D":
|
|
220
|
+
"""Apply coefficients to the expression and rebuild cached artifacts.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
coefs: Coefficient values matching the detected symbols.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
The current instance with coefficients applied.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
ExpressionError: If the number of coefficients differs from expectations.
|
|
230
|
+
"""
|
|
231
|
+
if len(coefs) != len(self.coefs):
|
|
232
|
+
raise ExpressionError(
|
|
233
|
+
"Number of arguments does not match the number of coefficients."
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
self.coefs = coefs
|
|
237
|
+
|
|
238
|
+
if any(
|
|
239
|
+
[
|
|
240
|
+
isinstance(x, (unc.core.Variable, unc.core.AffineScalarFunc))
|
|
241
|
+
for x in coefs
|
|
242
|
+
]
|
|
243
|
+
):
|
|
244
|
+
self._unc_coefs = True
|
|
245
|
+
|
|
246
|
+
coefs_dict = {}
|
|
247
|
+
for i in range(len(coefs)):
|
|
248
|
+
coefs_dict[self.args[i]] = coefs[i]
|
|
249
|
+
|
|
250
|
+
if self._unc_coefs:
|
|
251
|
+
# Separating nominal values and standard deviations.
|
|
252
|
+
nominal_coefs_dict = {}
|
|
253
|
+
std_coefs_dict = {}
|
|
254
|
+
for key in coefs_dict:
|
|
255
|
+
delta = sym.Symbol(f"Delta_{str(key)}")
|
|
256
|
+
if isinstance(
|
|
257
|
+
coefs_dict[key], (unc.core.Variable, unc.core.AffineScalarFunc)
|
|
258
|
+
):
|
|
259
|
+
nominal_coefs_dict[key] = coefs_dict[key].nominal_value
|
|
260
|
+
std_coefs_dict[delta] = coefs_dict[key].std_dev
|
|
261
|
+
else:
|
|
262
|
+
nominal_coefs_dict[key] = coefs_dict[key]
|
|
263
|
+
std_coefs_dict[delta] = 0
|
|
264
|
+
|
|
265
|
+
# making solutions
|
|
266
|
+
sols_list = []
|
|
267
|
+
for sol in self.sols:
|
|
268
|
+
# print(sol)
|
|
269
|
+
nominal = sol.subs(nominal_coefs_dict)
|
|
270
|
+
# print(nominal)
|
|
271
|
+
if nominal.is_complex and not nominal.is_real:
|
|
272
|
+
self._complex = True
|
|
273
|
+
|
|
274
|
+
if self._show_complex:
|
|
275
|
+
std = self._calculate_uncertainty_analyticaly(sol).subs(
|
|
276
|
+
{**nominal_coefs_dict, **std_coefs_dict}
|
|
277
|
+
)
|
|
278
|
+
real_part = unc.ufloat(sym.re(nominal), abs(sym.re(std)))
|
|
279
|
+
im_part = unc.ufloat(sym.im(nominal), abs(sym.im(std)))
|
|
280
|
+
sols_list.append((real_part, im_part))
|
|
281
|
+
|
|
282
|
+
else:
|
|
283
|
+
std = self._calculate_uncertainty_analyticaly(sol).subs(
|
|
284
|
+
{**nominal_coefs_dict, **std_coefs_dict}
|
|
285
|
+
)
|
|
286
|
+
sols_list.append(unc.ufloat(nominal, std))
|
|
287
|
+
|
|
288
|
+
self.sols = sols_list
|
|
289
|
+
|
|
290
|
+
# making lambda function
|
|
291
|
+
def wrap_lambdify(
|
|
292
|
+
func: Callable[..., LambdaReturn], *coefs: Coefficient
|
|
293
|
+
) -> Callable[[InputValue], LambdaReturn]:
|
|
294
|
+
def new_lambda(x: InputValue) -> LambdaReturn:
|
|
295
|
+
return func(x, *coefs)
|
|
296
|
+
|
|
297
|
+
return new_lambda
|
|
298
|
+
|
|
299
|
+
self.lambda_fun = wrap_lambdify(
|
|
300
|
+
sym.lambdify([sym.Symbol("x")] + self.args, self.expr_sym, "numpy"),
|
|
301
|
+
*coefs,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# making str expr
|
|
305
|
+
self.expr_unc = self.expr_str
|
|
306
|
+
for key in coefs_dict:
|
|
307
|
+
if isinstance(coefs_dict[key], unc.core.Variable):
|
|
308
|
+
replacement = f"unc.ufloat({coefs_dict[key].nominal_value},{coefs_dict[key].std_dev})"
|
|
309
|
+
else:
|
|
310
|
+
replacement = str(coefs_dict[key])
|
|
311
|
+
|
|
312
|
+
self.expr_unc = str(self.expr_unc).replace(str(key), replacement)
|
|
313
|
+
|
|
314
|
+
self._added_coefs = True
|
|
315
|
+
|
|
316
|
+
else:
|
|
317
|
+
self.expr_sym = self.expr_sym.subs(coefs_dict)
|
|
318
|
+
self.lambda_fun = sym.lambdify(sym.Symbol("x"), self.expr_sym, "numpy")
|
|
319
|
+
self._added_coefs = True
|
|
320
|
+
|
|
321
|
+
sols = self.sols
|
|
322
|
+
self.sols = [x.subs(coefs_dict) for x in sols]
|
|
323
|
+
if any([x.is_complex for x in self.sols]):
|
|
324
|
+
self._complex = True
|
|
325
|
+
|
|
326
|
+
return self
|
|
327
|
+
|
|
328
|
+
def to_latex_expr(self) -> str:
|
|
329
|
+
"""Convert the expression (with coefficients) to a LaTeX string.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
LaTeX-formatted expression string.
|
|
333
|
+
"""
|
|
334
|
+
latex = sym.latex(self.expr_sym)
|
|
335
|
+
|
|
336
|
+
if self._added_coefs:
|
|
337
|
+
for i in range(len(self.coefs)):
|
|
338
|
+
if isinstance(self.coefs[i], unc.core.Variable):
|
|
339
|
+
replacement = f"({self.coefs[i]})"
|
|
340
|
+
else:
|
|
341
|
+
replacement = f"{self.coefs[i]}"
|
|
342
|
+
|
|
343
|
+
latex = latex.replace(sym.latex(self.args[i]), replacement)
|
|
344
|
+
|
|
345
|
+
latex = latex.replace("+/-", r" \pm ")
|
|
346
|
+
latex = re.sub(
|
|
347
|
+
r"e([+-])(\d+)",
|
|
348
|
+
lambda m: rf"\cdot 10^{{{m.group(1)}{str(int(m.group(2)))}}}",
|
|
349
|
+
latex,
|
|
350
|
+
)
|
|
351
|
+
return rf"{latex}"
|
|
352
|
+
|
|
353
|
+
def _calculate_sols(self, show_unc: bool | None = None) -> list[str]:
|
|
354
|
+
"""Prepare LaTeX solution strings with optional uncertainty notation.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
show_unc: Whether to include propagated uncertainties.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
A list of LaTeX-formatted solution strings.
|
|
361
|
+
"""
|
|
362
|
+
latex_sols = []
|
|
363
|
+
|
|
364
|
+
if self._added_coefs:
|
|
365
|
+
if self._show_complex:
|
|
366
|
+
if self.sols:
|
|
367
|
+
del self.__dict__["sols"]
|
|
368
|
+
self.add_coefs(self.coefs)
|
|
369
|
+
latex_sols = [
|
|
370
|
+
str(x) if not isinstance(x, tuple) else f"({x[0]}) + ({x[1]}) i"
|
|
371
|
+
for x in self.sols
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
else:
|
|
375
|
+
latex_sols = self.sols
|
|
376
|
+
|
|
377
|
+
latex_sols = [f"{x}".replace("+/-", " \\pm ") for x in latex_sols]
|
|
378
|
+
|
|
379
|
+
else:
|
|
380
|
+
if show_unc:
|
|
381
|
+
if self.sols:
|
|
382
|
+
del self.__dict__["sols"]
|
|
383
|
+
|
|
384
|
+
latex_sols = [
|
|
385
|
+
f"{sym.latex(x)} + {sym.latex(self._calculate_uncertainty_analyticaly(x))}"
|
|
386
|
+
for x in self.sols
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
else:
|
|
390
|
+
if self.sols:
|
|
391
|
+
del self.__dict__["sols"]
|
|
392
|
+
latex_sols = [sym.latex(x) for x in self.sols]
|
|
393
|
+
|
|
394
|
+
return latex_sols
|
|
395
|
+
|
|
396
|
+
def to_latex_sols(
|
|
397
|
+
self, y: Coefficient | str | None = None, show_unc: bool = False
|
|
398
|
+
) -> str:
|
|
399
|
+
"""Format solutions as a LaTeX array with an optional target offset.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
y: Optional target value to subtract prior to solving.
|
|
403
|
+
show_unc: Whether to include uncertainties in the output.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
LaTeX-formatted solution array.
|
|
407
|
+
"""
|
|
408
|
+
show_unc = show_unc if not self._unc_coefs else self._unc_coefs
|
|
409
|
+
|
|
410
|
+
if not y:
|
|
411
|
+
latex_sols = self._calculate_sols(show_unc=show_unc)
|
|
412
|
+
elif isinstance(y, str):
|
|
413
|
+
new_expr = FunctionBase1D(str(self.expr_sym) + " - " + y)
|
|
414
|
+
latex_sols = new_expr._calculate_sols(show_unc=show_unc)
|
|
415
|
+
elif isinstance(y, unc.core.Variable) or isinstance(
|
|
416
|
+
y, unc.core.AffineScalarFunc
|
|
417
|
+
):
|
|
418
|
+
new_expr = FunctionBase1D(self.expr_str + " - A100")
|
|
419
|
+
new_expr.add_coefs(self.coefs + [y])
|
|
420
|
+
latex_sols = new_expr._calculate_sols(show_unc=show_unc)
|
|
421
|
+
else:
|
|
422
|
+
new_expr = FunctionBase1D(self.expr_str + "-" + str(y))
|
|
423
|
+
if self._added_coefs:
|
|
424
|
+
new_expr.add_coefs(self.coefs)
|
|
425
|
+
|
|
426
|
+
latex_sols = new_expr._calculate_sols(show_unc=show_unc)
|
|
427
|
+
|
|
428
|
+
for i, sol in enumerate(latex_sols):
|
|
429
|
+
latex_sols[i] = f"x_{i + 1} = {sol} \\\\"
|
|
430
|
+
|
|
431
|
+
lines = f"\\begin{{array}}{{l}}\n{'\n'.join(latex_sols)}\n\\end{{array}}"
|
|
432
|
+
lines = re.sub(
|
|
433
|
+
r"e([+-])(\d+)",
|
|
434
|
+
lambda m: rf"\cdot 10^{{{m.group(1)}{str(int(m.group(2)))}}}",
|
|
435
|
+
lines,
|
|
436
|
+
)
|
|
437
|
+
return lines
|
|
438
|
+
|
|
439
|
+
def deriv(self, level: int = 1) -> "FunctionBase1D":
|
|
440
|
+
"""Compute a derivative of the expression.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
level: Order of the derivative to compute.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
New instance representing the derivative.
|
|
447
|
+
"""
|
|
448
|
+
expr = self.expr_sym
|
|
449
|
+
|
|
450
|
+
deriv = sym.diff(expr, sym.Symbol("x"), level)
|
|
451
|
+
|
|
452
|
+
new_expr_str = str(deriv)
|
|
453
|
+
|
|
454
|
+
new_func = FunctionBase1D(new_expr_str)
|
|
455
|
+
|
|
456
|
+
if self._added_coefs:
|
|
457
|
+
new_func.add_coefs(self.coefs)
|
|
458
|
+
|
|
459
|
+
return new_func
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class Poly(FunctionBase1D):
|
|
463
|
+
"""Construct a polynomial expression of the specified degree."""
|
|
464
|
+
|
|
465
|
+
def __init__(self, degree: int = 1) -> None:
|
|
466
|
+
"""Create a polynomial expression of the specified degree.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
degree: Polynomial degree.
|
|
470
|
+
"""
|
|
471
|
+
self.degree = degree
|
|
472
|
+
|
|
473
|
+
expr_str = ""
|
|
474
|
+
for i in range(degree):
|
|
475
|
+
expr_str += f"p_{i}*x**{degree - i} + "
|
|
476
|
+
expr_str += f"p_{degree}"
|
|
477
|
+
|
|
478
|
+
super().__init__(expr_str)
|
|
479
|
+
# self.expr_str = expr_str
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
@cached_property
|
|
483
|
+
def sols(self) -> list[Solution]:
|
|
484
|
+
"""Return analytical solutions for the polynomial when solvable.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
A list of solutions for the polynomial equation.
|
|
488
|
+
|
|
489
|
+
Raises:
|
|
490
|
+
ExpressionError: If the polynomial degree exceeds four.
|
|
491
|
+
"""
|
|
492
|
+
if self.degree > 4:
|
|
493
|
+
raise ExpressionError(
|
|
494
|
+
"Cannot find analytical solutions for polynomials with degree > 4."
|
|
495
|
+
)
|
|
496
|
+
else:
|
|
497
|
+
return super().sols
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
class Hyper(FunctionBase1D):
|
|
501
|
+
"""Construct a hyperbolic expression variant."""
|
|
502
|
+
|
|
503
|
+
def __init__(self, style: int = 0) -> None:
|
|
504
|
+
"""Initialize a hyperbolic expression variant.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
style: Expression selector; ``0`` uses ``p_0*x / (x + p_1)``, otherwise
|
|
508
|
+
uses ``p_0 / (x + p_1) + p_2``.
|
|
509
|
+
"""
|
|
510
|
+
if style == 0:
|
|
511
|
+
expr_str = "p_0*x / (x + p_1)"
|
|
512
|
+
else:
|
|
513
|
+
expr_str = "p_0 / (x + p_1) + p_2"
|
|
514
|
+
|
|
515
|
+
super().__init__(expr_str)
|
|
516
|
+
return None
|
unc_tools/exceptions.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Custom exceptions for unc_tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"UncToolsError",
|
|
7
|
+
"DataError",
|
|
8
|
+
"ExpressionError",
|
|
9
|
+
"InitialGuessError",
|
|
10
|
+
"ModelTypeError",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UncToolsError(Exception):
|
|
15
|
+
"""Base exception for unc_tools."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DataError(ValueError, UncToolsError):
|
|
19
|
+
"""Raised when input data is missing or malformed."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ModelTypeError(TypeError, UncToolsError):
|
|
23
|
+
"""Raised when a model or callable has an unsupported type."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExpressionError(TypeError, UncToolsError):
|
|
27
|
+
"""Raised when an expression or coefficient set is invalid."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InitialGuessError(TypeError, UncToolsError):
|
|
31
|
+
"""Raised when a numerical solver lacks a valid initial guess."""
|