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 +15 -0
- physeq/cas/__init__.py +1 -0
- physeq/cas/equation.py +225 -0
- physeq/cas/exprorder.py +1774 -0
- physeq/cas/printing.py +404 -0
- physeq/cas/problem.py +279 -0
- physeq/cas/symbol.py +933 -0
- physeq/cas/wrapped.py +111 -0
- physeq/configuration.py +96 -0
- physeq/fmtversion.py +206 -0
- physeq/force.py +42 -0
- physeq/symbols/__init__.py +1 -0
- physeq/symbols/constants.py +24 -0
- physeq/symbols/force.py +85 -0
- physeq/symbols/kinematics.py +32 -0
- physeq/symbols/mass.py +23 -0
- physeq/symbols/space.py +36 -0
- physeq/symbols/time.py +17 -0
- physeq/symbols/work_energy.py +42 -0
- physeq/version.py +4 -0
- physeq/work_energy.py +37 -0
- physeq-0.1.0.dist-info/METADATA +241 -0
- physeq-0.1.0.dist-info/RECORD +26 -0
- physeq-0.1.0.dist-info/WHEEL +5 -0
- physeq-0.1.0.dist-info/licenses/LICENSE.txt +67 -0
- physeq-0.1.0.dist-info/top_level.txt +1 -0
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
|