physeq 0.1.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.
physeq/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (c) 2025, Geoffrey M. Poore
4
+ # All rights reserved.
5
+ #
6
+ # Licensed under the BSD 3-Clause License:
7
+ # http://opensource.org/licenses/BSD-3-Clause
8
+ #
9
+
10
+
11
+ from .version import __version__, __version_info__
12
+
13
+ from .configuration import Config
14
+ config = Config()
15
+ del Config
physeq/cas/__init__.py ADDED
@@ -0,0 +1 @@
1
+
physeq/cas/equation.py ADDED
@@ -0,0 +1,225 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (c) 2025-2026, Geoffrey M. Poore
4
+ # All rights reserved.
5
+ #
6
+ # Licensed under the BSD 3-Clause License:
7
+ # http://opensource.org/licenses/BSD-3-Clause
8
+ #
9
+
10
+
11
+ from __future__ import annotations
12
+
13
+ import math
14
+ import sympy
15
+ from astropy.units import Quantity
16
+ from typing import Literal, Self
17
+ from sympy.core.assumptions import check_assumptions as sympy_check_assumptions
18
+ from . import exprorder
19
+ from .symbol import ConstSymbol, Symbol, translate_numerical_xreplace_rule, translate_xreplace_rule
20
+
21
+
22
+
23
+
24
+ def solveset_with_checked_assumptions(
25
+ eq: sympy.Eq | exprorder.WrappedEq,
26
+ symbol: sympy.Symbol | exprorder.WrappedExpr,
27
+ domain=sympy.Complexes,
28
+ **assumptions: dict[str, bool | None],
29
+ ) -> sympy.FiniteSet:
30
+ '''
31
+ `solveset()` wrapper that only returns solutions consistent with `symbol`
32
+ assumptions (for example, `nonnegative` or `negative`). Additional
33
+ optional assumptions can be provided to further narrow the solutions.
34
+ '''
35
+ if isinstance(eq, exprorder.WrappedEq):
36
+ eq = eq.eq
37
+ elif not isinstance(eq, sympy.Eq):
38
+ raise TypeError
39
+ if isinstance(symbol, exprorder.WrappedExpr):
40
+ if not isinstance(symbol.expr, sympy.Symbol):
41
+ raise TypeError
42
+ symbol = symbol.expr
43
+ elif not isinstance(symbol, sympy.Symbol):
44
+ raise TypeError
45
+ if isinstance(symbol, ConstSymbol):
46
+ raise NotImplementedError
47
+
48
+ solution_set = sympy.solveset(eq, symbol, domain=domain)
49
+ if (isinstance(solution_set, sympy.Intersection) and len(solution_set.args) == 2 and
50
+ solution_set.args[0] is domain and isinstance(solution_set.args[1], sympy.FiniteSet)):
51
+ solution_set = solution_set.args[1]
52
+ if not isinstance(solution_set, sympy.FiniteSet):
53
+ raise NotImplementedError(f'Not "sympy.FiniteSet" but {type(solution_set)}: {solution_set}')
54
+ solution_set = sympy.FiniteSet(
55
+ *(x for x in solution_set if sympy_check_assumptions(x, symbol) is not False),
56
+ evaluate=False,
57
+ )
58
+ if assumptions:
59
+ solution_set = sympy.FiniteSet(
60
+ *(x for x in solution_set if sympy_check_assumptions(x, assumptions) is not False),
61
+ evaluate=False,
62
+ )
63
+ return solution_set
64
+
65
+
66
+ def solveset_for_ans(
67
+ eq: sympy.Eq | exprorder.WrappedEq,
68
+ symbol: sympy.Symbol | exprorder.WrappedExpr,
69
+ domain=sympy.Reals, # expanded support to include `Complexes`?
70
+ *,
71
+ ans: sympy.Number | sympy.NumberSymbol | int | float,
72
+ xreplace: dict[sympy.Symbol | exprorder.WrappedExpr,
73
+ sympy.Number | sympy.NumberSymbol | Quantity | ConstSymbol | int | float],
74
+ rel_tol: float | None = None,
75
+ abs_tol: float | None = None,
76
+ ) -> sympy.FiniteSet:
77
+ '''
78
+ `solveset()` wrapper that returns only the solution(s) that, when modified
79
+ with the given `.xreplace()` rule, yield a specified numerical answer, or
80
+ solutions that cannot be reduced to a number with the `.xreplace()` rule.
81
+ This is intended for determining which symbolic solution(s) yield a known
82
+ numerical answer.
83
+ '''
84
+ if isinstance(eq, exprorder.WrappedEq):
85
+ eq = eq.eq
86
+ elif not isinstance(eq, sympy.Eq):
87
+ raise TypeError
88
+ if isinstance(symbol, exprorder.WrappedExpr):
89
+ if not isinstance(symbol.expr, sympy.Symbol):
90
+ raise TypeError
91
+ symbol = symbol.expr
92
+ elif not isinstance(symbol, sympy.Symbol):
93
+ raise TypeError
94
+ if isinstance(ans, Quantity):
95
+ if isinstance(symbol, Symbol):
96
+ ans = symbol.quantity_value_in_si_coherent_unit(ans)
97
+ else:
98
+ raise TypeError(
99
+ f'"ans" can only be a Quantity when "{symbol}" is a physeq.Symbol with associated units'
100
+ )
101
+ elif not isinstance(ans, (sympy.Number, sympy.NumberSymbol, int, float)):
102
+ raise TypeError
103
+ if not isinstance(xreplace, dict):
104
+ raise TypeError
105
+ translated_xreplace = translate_numerical_xreplace_rule(xreplace)
106
+ # https://docs.python.org/3/library/math.html#math.isclose
107
+ if rel_tol is None:
108
+ rel_tol = 1e-09
109
+ if abs_tol is None:
110
+ abs_tol = 0.0
111
+
112
+ solution_set = solveset_with_checked_assumptions(eq, symbol, domain=domain)
113
+ filtered = []
114
+ for soln in solution_set:
115
+ soln_ans = soln.xreplace(translated_xreplace).n() # type: ignore
116
+ if soln_ans.is_Number:
117
+ if math.isclose(soln_ans, ans, rel_tol=rel_tol, abs_tol=abs_tol):
118
+ filtered.append(soln)
119
+ else:
120
+ # Keep solutions that can't be reduced to a number
121
+ filtered.append(soln)
122
+ return sympy.FiniteSet(*filtered, evaluate=False)
123
+
124
+
125
+
126
+
127
+ class Eq(sympy.Eq): # `sympy.Eq` is alias for `sympy.Equality`
128
+ '''
129
+ Subclass of `sympy.Eq` that is compatible with `exprorder.WrappedExpr` and
130
+ `astropy.units.Quantity`.
131
+
132
+ By default, `evaluate=False`.
133
+ '''
134
+
135
+ def __new__(cls, lhs, rhs, **options):
136
+ options.setdefault('evaluate', False)
137
+ if isinstance(lhs, exprorder.WrappedExpr):
138
+ lhs = lhs.expr
139
+ if isinstance(rhs, exprorder.WrappedExpr):
140
+ rhs = rhs.expr
141
+ obj = super().__new__(cls, lhs, rhs, **options)
142
+ if not isinstance(obj, cls):
143
+ raise NotImplementedError
144
+ return obj
145
+
146
+
147
+ def xreplace(self, raw_rule: dict, **kwargs) -> Self:
148
+ '''
149
+ `.xreplace()` compatible with `exprorder.WrappedExpr` and
150
+ `astropy.units.Quantity`.
151
+ '''
152
+ return super().xreplace(translate_xreplace_rule(raw_rule, **kwargs))
153
+
154
+ def num_xreplace(self, raw_rule: dict, **kwargs) -> Self:
155
+ '''
156
+ `.xreplace()` with purely numerical values compatible with PhysEq
157
+ `ConstSymbol`, `exprorder.WrappedExpr`, or `astropy.units.Quantity`.
158
+ '''
159
+ return super().xreplace(translate_numerical_xreplace_rule(raw_rule, **kwargs))
160
+
161
+ def subscript_with_xreplace_rule(
162
+ self, subscript: str | int, style: Literal['normal', 'italic', 'bold'] | None = None
163
+ ) -> tuple[Self, dict[Symbol, Symbol]]:
164
+ '''
165
+ Create a new `Eq` with subscripting for all PhysEq `Symbol` that
166
+ support it, and also return the `xreplace()` rule for performing the
167
+ subscripting replacements.
168
+ '''
169
+ rule = {}
170
+ for s in self.free_symbols:
171
+ if isinstance(s, Symbol) and s.is_subscriptable:
172
+ rule[s] = s.subscript(subscript, style)
173
+ return (self.xreplace(rule), rule)
174
+
175
+ def subscript(self, subscript: str | int, style: Literal['normal', 'italic', 'bold'] | None = None) -> Self:
176
+ '''
177
+ Create a new `Eq` with subscripting for all PhysEq `Symbol` that
178
+ support it.
179
+ '''
180
+ return self.subscript_with_xreplace_rule(subscript, style)[0]
181
+
182
+ @property
183
+ def nonconst_free_symbols(self) -> set[sympy.Symbol]:
184
+ '''
185
+ `.free_symbols` filtered to exclude PhysEq `ConstSymbol`.
186
+ '''
187
+ return set(x for x in self.free_symbols if not isinstance(x, ConstSymbol)) # type: ignore
188
+
189
+
190
+
191
+
192
+ class WrappedEq(exprorder.WrappedEq):
193
+ wrapped: Eq
194
+ eq: Eq
195
+
196
+ def __init__(self, *args, **kwargs):
197
+ super().__init__(*args, **kwargs)
198
+ if not isinstance(self.wrapped, Eq):
199
+ raise TypeError
200
+
201
+ def num_xreplace(self, *args, **kwargs) -> Self:
202
+ # This doesn't pass the xreplace rule to the new wrapped equation to
203
+ # generate new term orders. Order can typically be maintained
204
+ # sufficiently by setting `unevaluated=True`, or by printing with an
205
+ # xreplace rule.
206
+ new_eq = self.wrapped.num_xreplace(*args, **kwargs)
207
+ new_wrapped_eq = type(self)(new_eq, parents=self)
208
+ if self.wrapped_lhs and self.wrapped_rhs:
209
+ new_wrapped_eq.wrapped_lhs = self.wrapped_lhs.wrapper_class(new_eq.lhs, parents=self.wrapped_lhs)
210
+ new_wrapped_eq.wrapped_rhs = self.wrapped_rhs.wrapper_class(new_eq.rhs, parents=self.wrapped_rhs)
211
+ return new_wrapped_eq
212
+
213
+ def subscript(self, *args, **kwargs) -> Self:
214
+ new_eq, rule = self.wrapped.subscript_with_xreplace_rule(*args, **kwargs)
215
+ new_wrapped_eq = type(self)(new_eq, parents=self, xreplace_rule=rule)
216
+ if self.wrapped_lhs and self.wrapped_rhs:
217
+ new_wrapped_eq.wrapped_lhs = self.wrapped_lhs.wrapper_class(new_eq.lhs, parents=self.wrapped_lhs,
218
+ xreplace_rule=rule)
219
+ new_wrapped_eq.wrapped_rhs = self.wrapped_rhs.wrapper_class(new_eq.rhs, parents=self.wrapped_rhs,
220
+ xreplace_rule=rule)
221
+ return new_wrapped_eq
222
+
223
+ @property
224
+ def nonconst_free_symbols(self) -> set[sympy.Symbol]:
225
+ return self.wrapped.nonconst_free_symbols