funcprops 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.
- funcprops/__init__.py +234 -0
- funcprops/_semialg.py +246 -0
- funcprops/api.py +70 -0
- funcprops/branchcuts/__init__.py +141 -0
- funcprops/branchcuts/computation.py +664 -0
- funcprops/branchcuts/core.py +1204 -0
- funcprops/branchcuts/post_simplify.py +461 -0
- funcprops/conditions.py +124 -0
- funcprops/data/catalogue.json +127449 -0
- funcprops/data/dlmf_audit.json +3382 -0
- funcprops/dlmf.py +29 -0
- funcprops/engine.py +324 -0
- funcprops/exceptions.py +29 -0
- funcprops/proof_cache.py +373 -0
- funcprops/real_analysis/__init__.py +52 -0
- funcprops/real_analysis/advanced_convexity.py +217 -0
- funcprops/real_analysis/boundedness.py +168 -0
- funcprops/real_analysis/common.py +244 -0
- funcprops/real_analysis/convexity.py +698 -0
- funcprops/real_analysis/differentiability.py +112 -0
- funcprops/real_analysis/domain.py +188 -0
- funcprops/real_analysis/mapping.py +374 -0
- funcprops/real_analysis/monotonic_certificates.py +867 -0
- funcprops/real_analysis/monotonicity.py +673 -0
- funcprops/real_analysis/oscillation.py +151 -0
- funcprops/real_analysis/piecewise.py +174 -0
- funcprops/real_analysis/range.py +552 -0
- funcprops/real_analysis/sign.py +561 -0
- funcprops/real_analysis/smoothness.py +268 -0
- funcprops/real_analysis/special_functions.py +515 -0
- funcprops/real_analysis/structural_properties.py +558 -0
- funcprops/real_analysis/zeros.py +327 -0
- funcprops/registry.py +274 -0
- funcprops/results.py +239 -0
- funcprops/singularities.py +1767 -0
- funcprops/stratification.py +494 -0
- funcprops/sympy_compare.py +63 -0
- funcprops/transcendental/__init__.py +140 -0
- funcprops/transcendental/algebraize.py +201 -0
- funcprops/transcendental/analytic.py +261 -0
- funcprops/transcendental/complex_roots.py +542 -0
- funcprops/transcendental/exp_fourier.py +221 -0
- funcprops/transcendental/extrema.py +821 -0
- funcprops/transcendental/periodic.py +222 -0
- funcprops/transcendental/proof.py +104 -0
- funcprops/transcendental/real_interval.py +128 -0
- funcprops/transcendental/real_sign.py +117 -0
- funcprops/transcendental/reduce.py +288 -0
- funcprops/transcendental/roots.py +1380 -0
- funcprops/transcendental/zero_geometry.py +1197 -0
- funcprops/verification.py +410 -0
- funcprops-0.1.0.dist-info/METADATA +208 -0
- funcprops-0.1.0.dist-info/RECORD +56 -0
- funcprops-0.1.0.dist-info/WHEEL +5 -0
- funcprops-0.1.0.dist-info/licenses/LICENSE +674 -0
- funcprops-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import sympy as sp
|
|
6
|
+
from sympy import S
|
|
7
|
+
from sympy.polys.polyerrors import PolynomialError
|
|
8
|
+
|
|
9
|
+
from ..engine import simplify_condition
|
|
10
|
+
from ..results import Homogeneity, Periodicity, Symmetry
|
|
11
|
+
from ..transcendental.periodic import common_period_values, positive_period
|
|
12
|
+
from .common import normalize_variables
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class PeriodicityResult:
|
|
17
|
+
classification: Periodicity
|
|
18
|
+
period: sp.Expr | None = None
|
|
19
|
+
domain: object = S.Reals
|
|
20
|
+
evidence: tuple = ()
|
|
21
|
+
fundamental: bool | None = None
|
|
22
|
+
conditions: sp.Expr = S.true
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class SymmetryResult:
|
|
27
|
+
classification: Symmetry
|
|
28
|
+
center: sp.Expr = S.Zero
|
|
29
|
+
domain: object = S.Reals
|
|
30
|
+
evidence: tuple = ()
|
|
31
|
+
centers: object = ()
|
|
32
|
+
center_period: sp.Expr | None = None
|
|
33
|
+
central_value: sp.Expr | None = None
|
|
34
|
+
center_families: tuple = ()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class HomogeneityCase:
|
|
39
|
+
degree: sp.Expr | None
|
|
40
|
+
condition: sp.Expr
|
|
41
|
+
weights: tuple
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class HomogeneityResult:
|
|
46
|
+
classification: Homogeneity
|
|
47
|
+
degree: sp.Expr | None = None
|
|
48
|
+
variables: tuple = ()
|
|
49
|
+
evidence: tuple = ()
|
|
50
|
+
weights: tuple = ()
|
|
51
|
+
conditions: sp.Expr = S.true
|
|
52
|
+
cases: tuple[HomogeneityCase, ...] = ()
|
|
53
|
+
assumptions: sp.Expr = S.true
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _zero(expr):
|
|
57
|
+
q = sp.simplify(expr)
|
|
58
|
+
if q == 0 or q.is_zero is True:
|
|
59
|
+
return True
|
|
60
|
+
if q.is_zero is False:
|
|
61
|
+
return False
|
|
62
|
+
try:
|
|
63
|
+
eq = q.equals(0)
|
|
64
|
+
if eq is True:
|
|
65
|
+
return True
|
|
66
|
+
if eq is False:
|
|
67
|
+
return False
|
|
68
|
+
except (TypeError, ValueError, NotImplementedError):
|
|
69
|
+
pass
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _period_from_frequency(arg, var):
|
|
74
|
+
"""Return exact positive period for sin/cos-like affine arguments."""
|
|
75
|
+
try:
|
|
76
|
+
a = sp.simplify(sp.diff(arg, var))
|
|
77
|
+
if a.has(var) or a == 0:
|
|
78
|
+
return None
|
|
79
|
+
if sp.simplify(sp.diff(arg, var, 2)) != 0:
|
|
80
|
+
return None
|
|
81
|
+
p = sp.simplify(2 * sp.pi / sp.Abs(a))
|
|
82
|
+
return p if p.is_positive is True else None
|
|
83
|
+
except (TypeError, ValueError, NotImplementedError):
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _period_of(expr, var):
|
|
88
|
+
"""Conservative exact period propagation; returned period need not be minimal."""
|
|
89
|
+
expr = sp.sympify(expr)
|
|
90
|
+
if not expr.has(var):
|
|
91
|
+
return None
|
|
92
|
+
p = positive_period(expr, var)
|
|
93
|
+
if p is not None:
|
|
94
|
+
return p
|
|
95
|
+
if expr.func in (sp.sin, sp.cos) and len(expr.args) == 1:
|
|
96
|
+
return _period_from_frequency(expr.args[0], var)
|
|
97
|
+
if expr.func in (sp.tan, sp.cot) and len(expr.args) == 1:
|
|
98
|
+
q = _period_from_frequency(expr.args[0], var)
|
|
99
|
+
return sp.simplify(q / 2) if q is not None else None
|
|
100
|
+
# Any pointwise outer function of a vector of periodic arguments inherits
|
|
101
|
+
# every common period of those arguments. Constants do not constrain it.
|
|
102
|
+
if expr.is_Function or (expr.func not in (sp.Add, sp.Mul, sp.Pow) and expr.args):
|
|
103
|
+
varying = [a for a in expr.args if a.has(var)]
|
|
104
|
+
if varying:
|
|
105
|
+
ps = [_period_of(a, var) for a in varying]
|
|
106
|
+
if all(q is not None for q in ps):
|
|
107
|
+
return common_period_values(*ps)
|
|
108
|
+
if expr.is_Add or expr.is_Mul:
|
|
109
|
+
varying = [a for a in expr.args if a.has(var)]
|
|
110
|
+
ps = [_period_of(a, var) for a in varying]
|
|
111
|
+
if varying and all(q is not None for q in ps):
|
|
112
|
+
return common_period_values(*ps)
|
|
113
|
+
if expr.is_Pow and not expr.exp.has(var):
|
|
114
|
+
return _period_of(expr.base, var)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _visible_incommensurate_periods(expr, var):
|
|
119
|
+
if not (expr.is_Add or expr.is_Mul):
|
|
120
|
+
return False
|
|
121
|
+
ps = []
|
|
122
|
+
for a in expr.args:
|
|
123
|
+
if not a.has(var):
|
|
124
|
+
continue
|
|
125
|
+
p = _period_of(a, var)
|
|
126
|
+
if p is None:
|
|
127
|
+
return False
|
|
128
|
+
ps.append(p)
|
|
129
|
+
return len(ps) > 1 and common_period_values(*ps) is None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def function_periodicity(expr, var, *, domain=True, assumptions=True, details=False):
|
|
133
|
+
"""Classify exact real periodicity and return a certified period in detail mode.
|
|
134
|
+
|
|
135
|
+
A period propagated through composition/sums/products is certified but is
|
|
136
|
+
not claimed fundamental unless the underlying exact periodicity engine
|
|
137
|
+
establishes the same period directly.
|
|
138
|
+
"""
|
|
139
|
+
expr = sp.sympify(expr)
|
|
140
|
+
var = sp.sympify(var)
|
|
141
|
+
if not expr.has(var):
|
|
142
|
+
r = PeriodicityResult(
|
|
143
|
+
Periodicity.CONSTANT, None, domain, ("independent-of-variable",), False
|
|
144
|
+
)
|
|
145
|
+
return r if details else r.classification
|
|
146
|
+
direct = positive_period(expr, var)
|
|
147
|
+
p = direct if direct is not None else _period_of(expr, var)
|
|
148
|
+
# Preserve a symbolic affine-frequency theorem instead of degrading to
|
|
149
|
+
# Unknown merely because the frequency may vanish on a parameter stratum.
|
|
150
|
+
if (
|
|
151
|
+
p is None
|
|
152
|
+
and expr.func in (sp.sin, sp.cos, sp.tan, sp.cot)
|
|
153
|
+
and len(expr.args) == 1
|
|
154
|
+
):
|
|
155
|
+
arg = expr.args[0]
|
|
156
|
+
try:
|
|
157
|
+
a = sp.simplify(sp.diff(arg, var))
|
|
158
|
+
b = sp.simplify(arg - a * var)
|
|
159
|
+
affine = (
|
|
160
|
+
not a.has(var)
|
|
161
|
+
and sp.simplify(sp.diff(arg, var, 2)) == 0
|
|
162
|
+
and not b.has(var)
|
|
163
|
+
)
|
|
164
|
+
except (TypeError, ValueError, NotImplementedError):
|
|
165
|
+
affine = False
|
|
166
|
+
a = None
|
|
167
|
+
if affine and a is not None and a.is_real is not False and a != 0:
|
|
168
|
+
zero_guard = simplify_condition(sp.Eq(a, 0), assumptions=assumptions)
|
|
169
|
+
nonzero_guard = simplify_condition(sp.Ne(a, 0), assumptions=assumptions)
|
|
170
|
+
if zero_guard is S.true:
|
|
171
|
+
r = PeriodicityResult(
|
|
172
|
+
Periodicity.CONSTANT,
|
|
173
|
+
None,
|
|
174
|
+
domain,
|
|
175
|
+
("zero-affine-frequency",),
|
|
176
|
+
False,
|
|
177
|
+
)
|
|
178
|
+
return r if details else r.classification
|
|
179
|
+
base = sp.pi if expr.func in (sp.tan, sp.cot) else 2 * sp.pi
|
|
180
|
+
cp = sp.simplify(base / sp.Abs(a))
|
|
181
|
+
cond = S.true if nonzero_guard is S.true else sp.Ne(a, 0)
|
|
182
|
+
evidence = (
|
|
183
|
+
("affine-frequency",)
|
|
184
|
+
if cond is S.true
|
|
185
|
+
else ("conditional-affine-frequency",)
|
|
186
|
+
)
|
|
187
|
+
r = PeriodicityResult(
|
|
188
|
+
Periodicity.PERIODIC, cp, domain, evidence, None, cond
|
|
189
|
+
)
|
|
190
|
+
return r if details else r.classification
|
|
191
|
+
if p is not None:
|
|
192
|
+
# Replay the shift identity before accepting structural propagation.
|
|
193
|
+
z = _zero(expr.subs(var, var + p) - expr)
|
|
194
|
+
if z is True:
|
|
195
|
+
primitive_minimal = (
|
|
196
|
+
expr.func in (sp.sin, sp.cos, sp.tan, sp.cot)
|
|
197
|
+
and len(expr.args) == 1
|
|
198
|
+
and _period_from_frequency(expr.args[0], var) is not None
|
|
199
|
+
)
|
|
200
|
+
fundamental = (
|
|
201
|
+
True
|
|
202
|
+
if primitive_minimal
|
|
203
|
+
and direct is not None
|
|
204
|
+
and sp.simplify(direct - p) == 0
|
|
205
|
+
else None
|
|
206
|
+
)
|
|
207
|
+
ev = (
|
|
208
|
+
("exact-periodicity",)
|
|
209
|
+
if direct is not None
|
|
210
|
+
else ("commensurate-period-propagation",)
|
|
211
|
+
)
|
|
212
|
+
r = PeriodicityResult(
|
|
213
|
+
Periodicity.PERIODIC, sp.simplify(p), domain, ev, fundamental
|
|
214
|
+
)
|
|
215
|
+
return r if details else r.classification
|
|
216
|
+
try:
|
|
217
|
+
if expr.is_polynomial(var) and sp.Poly(expr, var).degree() > 0:
|
|
218
|
+
result = PeriodicityResult(
|
|
219
|
+
Periodicity.APERIODIC,
|
|
220
|
+
None,
|
|
221
|
+
domain,
|
|
222
|
+
("nonconstant-polynomial",),
|
|
223
|
+
None,
|
|
224
|
+
)
|
|
225
|
+
return result if details else result.classification
|
|
226
|
+
except PolynomialError:
|
|
227
|
+
pass
|
|
228
|
+
if _visible_incommensurate_periods(expr, var):
|
|
229
|
+
r = PeriodicityResult(
|
|
230
|
+
Periodicity.APERIODIC,
|
|
231
|
+
None,
|
|
232
|
+
domain,
|
|
233
|
+
("incommensurate-component-periods",),
|
|
234
|
+
None,
|
|
235
|
+
)
|
|
236
|
+
return r if details else r.classification
|
|
237
|
+
r = PeriodicityResult(Periodicity.UNKNOWN, None, domain, ())
|
|
238
|
+
return r if details else r.classification
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _classify_about(expr, var, c, *, allow_central=False):
|
|
242
|
+
reflected = sp.simplify(expr.subs(var, 2 * c - var))
|
|
243
|
+
even = _zero(reflected - expr)
|
|
244
|
+
odd = _zero(reflected + expr)
|
|
245
|
+
k = sp.simplify(expr.subs(var, c))
|
|
246
|
+
central = _zero(reflected + expr - 2 * k) if k is not None else None
|
|
247
|
+
if even is True and odd is True:
|
|
248
|
+
cls = Symmetry.BOTH
|
|
249
|
+
elif even is True:
|
|
250
|
+
cls = Symmetry.EVEN
|
|
251
|
+
elif odd is True:
|
|
252
|
+
cls = Symmetry.ODD
|
|
253
|
+
elif allow_central and central is True:
|
|
254
|
+
cls = Symmetry.CENTRAL
|
|
255
|
+
elif even is False and odd is False and (central is False or not allow_central):
|
|
256
|
+
cls = Symmetry.NEITHER
|
|
257
|
+
else:
|
|
258
|
+
cls = Symmetry.UNKNOWN
|
|
259
|
+
return cls, reflected, k
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _polynomial_reflection_center(expr, var):
|
|
263
|
+
try:
|
|
264
|
+
p = sp.Poly(expr, var)
|
|
265
|
+
n = p.degree()
|
|
266
|
+
if n <= 0:
|
|
267
|
+
return S.Zero
|
|
268
|
+
if n == 1:
|
|
269
|
+
return ("all-centers-central",)
|
|
270
|
+
c = sp.Dummy("_center", real=True)
|
|
271
|
+
t = sp.Dummy("_offset", real=True)
|
|
272
|
+
shifted = sp.Poly(sp.expand(expr.subs(var, c + t)), t)
|
|
273
|
+
# Even/reflection symmetry requires all odd powers to vanish. Central
|
|
274
|
+
# symmetry requires all positive even powers to vanish. Solve either
|
|
275
|
+
# exact coefficient system and replay on the original expression.
|
|
276
|
+
systems = []
|
|
277
|
+
odd = [coef for (m,), coef in shifted.terms() if m % 2]
|
|
278
|
+
evenpos = [coef for (m,), coef in shifted.terms() if m > 0 and m % 2 == 0]
|
|
279
|
+
if odd:
|
|
280
|
+
systems.append(odd)
|
|
281
|
+
if evenpos:
|
|
282
|
+
systems.append(evenpos)
|
|
283
|
+
vals = []
|
|
284
|
+
for equations in systems:
|
|
285
|
+
sol = sp.solve(equations, c, dict=False)
|
|
286
|
+
if isinstance(sol, dict):
|
|
287
|
+
sol = [sol.get(c)]
|
|
288
|
+
if not isinstance(sol, (tuple, list, set)):
|
|
289
|
+
sol = [sol]
|
|
290
|
+
for q in sol:
|
|
291
|
+
if isinstance(q, (tuple, list)):
|
|
292
|
+
q = q[0] if len(q) == 1 else None
|
|
293
|
+
if q is not None and not sp.sympify(q).has(c):
|
|
294
|
+
vals.append(sp.simplify(q))
|
|
295
|
+
vals = tuple(dict.fromkeys(vals))
|
|
296
|
+
return vals[0] if len(vals) == 1 else vals
|
|
297
|
+
except (PolynomialError, TypeError, ValueError, NotImplementedError):
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _trig_center_family(expr, var):
|
|
302
|
+
# For a single affine sin/cos, return a representative reflection center
|
|
303
|
+
# and period of equivalent centers. This remains exact and compact.
|
|
304
|
+
if expr.func not in (sp.sin, sp.cos) or len(expr.args) != 1:
|
|
305
|
+
return None
|
|
306
|
+
arg = expr.args[0]
|
|
307
|
+
try:
|
|
308
|
+
a = sp.simplify(sp.diff(arg, var))
|
|
309
|
+
b = sp.simplify(arg - a * var)
|
|
310
|
+
if a.has(var) or a == 0:
|
|
311
|
+
return None
|
|
312
|
+
step = sp.simplify(sp.pi / sp.Abs(a))
|
|
313
|
+
c0 = sp.simplify(-b / a)
|
|
314
|
+
half = sp.simplify(sp.pi / (2 * a))
|
|
315
|
+
if expr.func is sp.cos:
|
|
316
|
+
cls = Symmetry.EVEN
|
|
317
|
+
families = (
|
|
318
|
+
(Symmetry.EVEN, c0, step),
|
|
319
|
+
(Symmetry.ODD, sp.simplify(c0 + half), step),
|
|
320
|
+
)
|
|
321
|
+
else:
|
|
322
|
+
cls = Symmetry.ODD
|
|
323
|
+
families = (
|
|
324
|
+
(Symmetry.ODD, c0, step),
|
|
325
|
+
(Symmetry.EVEN, sp.simplify(c0 + half), step),
|
|
326
|
+
)
|
|
327
|
+
return c0, step, cls, families
|
|
328
|
+
except (TypeError, ValueError, NotImplementedError):
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def function_symmetry(expr, var, *, center=0, domain=True, details=False):
|
|
333
|
+
"""Classify reflection symmetry about ``center``.
|
|
334
|
+
|
|
335
|
+
Passing ``center=None`` requests exact center discovery. Detail mode also
|
|
336
|
+
reports periodic center families when a compact exact family is known.
|
|
337
|
+
"""
|
|
338
|
+
expr = sp.sympify(expr)
|
|
339
|
+
var = sp.sympify(var)
|
|
340
|
+
if center is None:
|
|
341
|
+
fam = _trig_center_family(expr, var)
|
|
342
|
+
if fam is not None:
|
|
343
|
+
c, step, cls, families = fam
|
|
344
|
+
got, _, k = _classify_about(expr, var, c, allow_central=True)
|
|
345
|
+
if got is cls:
|
|
346
|
+
r = SymmetryResult(
|
|
347
|
+
cls,
|
|
348
|
+
c,
|
|
349
|
+
domain,
|
|
350
|
+
("affine-trigonometric-center",),
|
|
351
|
+
(),
|
|
352
|
+
step,
|
|
353
|
+
k,
|
|
354
|
+
families,
|
|
355
|
+
)
|
|
356
|
+
return r if details else cls
|
|
357
|
+
pc = _polynomial_reflection_center(expr, var)
|
|
358
|
+
if isinstance(pc, tuple) and pc == ("all-centers-central",):
|
|
359
|
+
c = S.Zero
|
|
360
|
+
cls, ref, k = _classify_about(expr, var, c, allow_central=True)
|
|
361
|
+
r = SymmetryResult(
|
|
362
|
+
cls, c, domain, ("affine-all-centers-central",), S.Reals, None, k, ()
|
|
363
|
+
)
|
|
364
|
+
return r if details else cls
|
|
365
|
+
candidates = pc if isinstance(pc, tuple) else (() if pc is None else (pc,))
|
|
366
|
+
# A pointwise outer function preserves every even reflection center of
|
|
367
|
+
# its varying argument. Verify on the full expression before accepting.
|
|
368
|
+
if not candidates and len([a for a in expr.args if a.has(var)]) == 1:
|
|
369
|
+
inner = next(a for a in expr.args if a.has(var))
|
|
370
|
+
ir = function_symmetry(inner, var, center=None, domain=domain, details=True)
|
|
371
|
+
if ir.classification in (Symmetry.EVEN, Symmetry.BOTH):
|
|
372
|
+
candidates = (ir.center,)
|
|
373
|
+
for c in candidates:
|
|
374
|
+
cls, ref, k = _classify_about(expr, var, c, allow_central=True)
|
|
375
|
+
if cls in (Symmetry.EVEN, Symmetry.ODD, Symmetry.BOTH, Symmetry.CENTRAL):
|
|
376
|
+
r = SymmetryResult(
|
|
377
|
+
cls,
|
|
378
|
+
c,
|
|
379
|
+
domain,
|
|
380
|
+
(sp.Eq(ref, expr), sp.Eq(ref, -expr)),
|
|
381
|
+
candidates,
|
|
382
|
+
None,
|
|
383
|
+
k,
|
|
384
|
+
(),
|
|
385
|
+
)
|
|
386
|
+
return r if details else cls
|
|
387
|
+
r = SymmetryResult(Symmetry.UNKNOWN, S.Zero, domain, (), candidates, None)
|
|
388
|
+
return r if details else r.classification
|
|
389
|
+
c = sp.sympify(center)
|
|
390
|
+
cls, reflected, k = _classify_about(expr, var, c)
|
|
391
|
+
r = SymmetryResult(
|
|
392
|
+
cls,
|
|
393
|
+
c,
|
|
394
|
+
domain,
|
|
395
|
+
(sp.Eq(reflected, expr), sp.Eq(reflected, -expr)),
|
|
396
|
+
(c,),
|
|
397
|
+
None,
|
|
398
|
+
k,
|
|
399
|
+
(),
|
|
400
|
+
)
|
|
401
|
+
return r if details else cls
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _normalize_weights(vars_, weights):
|
|
405
|
+
if weights is None:
|
|
406
|
+
return tuple(S.One for _ in vars_)
|
|
407
|
+
if isinstance(weights, dict):
|
|
408
|
+
return tuple(sp.sympify(weights.get(v, S.One)) for v in vars_)
|
|
409
|
+
ww = tuple(sp.sympify(w) for w in weights)
|
|
410
|
+
if len(ww) != len(vars_):
|
|
411
|
+
raise ValueError("weights must match variables")
|
|
412
|
+
return ww
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _weighted_poly_cases(expr, vars_, weights):
|
|
416
|
+
"""Return exact degree and parameter strata for a polynomial."""
|
|
417
|
+
try:
|
|
418
|
+
polynomial = sp.Poly(expr, *vars_)
|
|
419
|
+
except PolynomialError:
|
|
420
|
+
return None
|
|
421
|
+
p = polynomial
|
|
422
|
+
if p.is_zero:
|
|
423
|
+
return (HomogeneityCase(None, S.true, weights),)
|
|
424
|
+
grouped = {}
|
|
425
|
+
for monom, coeff in p.terms():
|
|
426
|
+
d = sp.simplify(
|
|
427
|
+
sum(sp.Integer(m) * w for m, w in zip(monom, weights, strict=False))
|
|
428
|
+
)
|
|
429
|
+
grouped.setdefault(d, []).append(coeff)
|
|
430
|
+
if len(grouped) == 1:
|
|
431
|
+
d = next(iter(grouped))
|
|
432
|
+
return (HomogeneityCase(d, S.true, weights),)
|
|
433
|
+
cases = []
|
|
434
|
+
all_coeffs = [sp.simplify(c) for coeffs in grouped.values() for c in coeffs]
|
|
435
|
+
zero_cond = sp.And(*(sp.Eq(c, 0) for c in all_coeffs)) if all_coeffs else S.true
|
|
436
|
+
cases.append(HomogeneityCase(None, zero_cond, weights))
|
|
437
|
+
for d in grouped:
|
|
438
|
+
vanish = []
|
|
439
|
+
for od, coeffs in grouped.items():
|
|
440
|
+
if od == d:
|
|
441
|
+
continue
|
|
442
|
+
vanish.extend(sp.Eq(sp.simplify(c), 0) for c in coeffs)
|
|
443
|
+
# A weighted-degree block survives when *at least one* coefficient
|
|
444
|
+
# in that block is nonzero. Requiring every coefficient to be nonzero
|
|
445
|
+
# incorrectly drops homogeneous parameter strata such as a*x + b*y
|
|
446
|
+
# with a=0, b!=0.
|
|
447
|
+
keep_terms = [sp.Ne(sp.simplify(c), 0) for c in grouped[d]]
|
|
448
|
+
keep = sp.Or(*keep_terms) if keep_terms else S.false
|
|
449
|
+
cond = sp.And(*([*vanish, keep])) if (vanish or keep_terms) else S.true
|
|
450
|
+
cases.append(HomogeneityCase(d, cond, weights))
|
|
451
|
+
return tuple(cases)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _infer_homogeneous_degree(expr, vars_, weights):
|
|
455
|
+
cases = _weighted_poly_cases(expr, vars_, weights)
|
|
456
|
+
if cases is not None:
|
|
457
|
+
if len(cases) == 1 and cases[0].condition is S.true:
|
|
458
|
+
return cases[0].degree, cases
|
|
459
|
+
return False, cases
|
|
460
|
+
if expr == 0:
|
|
461
|
+
return None, (HomogeneityCase(None, S.true, weights),)
|
|
462
|
+
euler = sp.simplify(
|
|
463
|
+
sum(w * v * sp.diff(expr, v) for v, w in zip(vars_, weights, strict=False))
|
|
464
|
+
)
|
|
465
|
+
ratio = sp.cancel(euler / expr)
|
|
466
|
+
if not any(ratio.has(v) for v in vars_) and ratio.is_real is not False:
|
|
467
|
+
if _zero(euler - ratio * expr) is True:
|
|
468
|
+
return sp.simplify(ratio), (
|
|
469
|
+
HomogeneityCase(sp.simplify(ratio), S.true, weights),
|
|
470
|
+
)
|
|
471
|
+
return None, ()
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def function_homogeneity(
|
|
475
|
+
expr, vars, *, degree=None, weights=None, assumptions=True, details=False
|
|
476
|
+
):
|
|
477
|
+
"""Classify positive weighted homogeneity in one or more variables.
|
|
478
|
+
|
|
479
|
+
``weights`` defaults to ordinary simultaneous scaling. Detail mode retains
|
|
480
|
+
exact parameter strata when a polynomial becomes homogeneous only after
|
|
481
|
+
some coefficients vanish.
|
|
482
|
+
"""
|
|
483
|
+
expr = sp.sympify(expr)
|
|
484
|
+
vv = normalize_variables(vars)
|
|
485
|
+
ww = _normalize_weights(vv, weights)
|
|
486
|
+
inferred, cases = _infer_homogeneous_degree(expr, vv, ww)
|
|
487
|
+
assumptions = sp.sympify(assumptions)
|
|
488
|
+
if expr == 0:
|
|
489
|
+
if degree is not None:
|
|
490
|
+
d = sp.sympify(degree)
|
|
491
|
+
cls = Homogeneity.HOMOGENEOUS
|
|
492
|
+
rd = d
|
|
493
|
+
else:
|
|
494
|
+
cls = Homogeneity.HOMOGENEOUS
|
|
495
|
+
rd = None
|
|
496
|
+
r = HomogeneityResult(
|
|
497
|
+
cls, rd, vv, ("zero-function-all-degrees",), ww, S.true, cases, assumptions
|
|
498
|
+
)
|
|
499
|
+
return r if details else cls
|
|
500
|
+
if degree is not None:
|
|
501
|
+
d = sp.sympify(degree)
|
|
502
|
+
if inferred not in (None, False):
|
|
503
|
+
z = _zero(inferred - d)
|
|
504
|
+
cls = (
|
|
505
|
+
Homogeneity.HOMOGENEOUS
|
|
506
|
+
if z is True
|
|
507
|
+
else Homogeneity.NOT_HOMOGENEOUS
|
|
508
|
+
if z is False
|
|
509
|
+
else Homogeneity.UNKNOWN
|
|
510
|
+
)
|
|
511
|
+
else:
|
|
512
|
+
t = sp.Dummy("_scale", positive=True)
|
|
513
|
+
scaled = expr.xreplace({v: t**w * v for v, w in zip(vv, ww, strict=False)})
|
|
514
|
+
z = _zero(scaled - t**d * expr)
|
|
515
|
+
cls = (
|
|
516
|
+
Homogeneity.HOMOGENEOUS
|
|
517
|
+
if z is True
|
|
518
|
+
else Homogeneity.NOT_HOMOGENEOUS
|
|
519
|
+
if z is False
|
|
520
|
+
else Homogeneity.UNKNOWN
|
|
521
|
+
)
|
|
522
|
+
rd = d if cls is Homogeneity.HOMOGENEOUS else None
|
|
523
|
+
else:
|
|
524
|
+
if inferred is False:
|
|
525
|
+
applicable = []
|
|
526
|
+
for case in cases:
|
|
527
|
+
try:
|
|
528
|
+
implication = sp.simplify(sp.Implies(assumptions, case.condition))
|
|
529
|
+
except (TypeError, ValueError, NotImplementedError):
|
|
530
|
+
implication = None
|
|
531
|
+
if implication is S.true:
|
|
532
|
+
applicable.append(case)
|
|
533
|
+
if len(applicable) == 1:
|
|
534
|
+
cls = Homogeneity.HOMOGENEOUS
|
|
535
|
+
rd = applicable[0].degree
|
|
536
|
+
else:
|
|
537
|
+
cls = Homogeneity.NOT_HOMOGENEOUS
|
|
538
|
+
rd = None
|
|
539
|
+
elif inferred is None:
|
|
540
|
+
cls = Homogeneity.UNKNOWN
|
|
541
|
+
rd = None
|
|
542
|
+
else:
|
|
543
|
+
cls = Homogeneity.HOMOGENEOUS
|
|
544
|
+
rd = sp.sympify(inferred)
|
|
545
|
+
cond = S.true
|
|
546
|
+
if cases and cls is not Homogeneity.HOMOGENEOUS:
|
|
547
|
+
cond = sp.Or(*(c.condition for c in cases))
|
|
548
|
+
r = HomogeneityResult(
|
|
549
|
+
cls,
|
|
550
|
+
rd,
|
|
551
|
+
vv,
|
|
552
|
+
("weighted-Euler-homogeneity",) if cls is Homogeneity.HOMOGENEOUS else (),
|
|
553
|
+
ww,
|
|
554
|
+
cond,
|
|
555
|
+
cases,
|
|
556
|
+
assumptions,
|
|
557
|
+
)
|
|
558
|
+
return r if details else cls
|