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.
Files changed (56) hide show
  1. funcprops/__init__.py +234 -0
  2. funcprops/_semialg.py +246 -0
  3. funcprops/api.py +70 -0
  4. funcprops/branchcuts/__init__.py +141 -0
  5. funcprops/branchcuts/computation.py +664 -0
  6. funcprops/branchcuts/core.py +1204 -0
  7. funcprops/branchcuts/post_simplify.py +461 -0
  8. funcprops/conditions.py +124 -0
  9. funcprops/data/catalogue.json +127449 -0
  10. funcprops/data/dlmf_audit.json +3382 -0
  11. funcprops/dlmf.py +29 -0
  12. funcprops/engine.py +324 -0
  13. funcprops/exceptions.py +29 -0
  14. funcprops/proof_cache.py +373 -0
  15. funcprops/real_analysis/__init__.py +52 -0
  16. funcprops/real_analysis/advanced_convexity.py +217 -0
  17. funcprops/real_analysis/boundedness.py +168 -0
  18. funcprops/real_analysis/common.py +244 -0
  19. funcprops/real_analysis/convexity.py +698 -0
  20. funcprops/real_analysis/differentiability.py +112 -0
  21. funcprops/real_analysis/domain.py +188 -0
  22. funcprops/real_analysis/mapping.py +374 -0
  23. funcprops/real_analysis/monotonic_certificates.py +867 -0
  24. funcprops/real_analysis/monotonicity.py +673 -0
  25. funcprops/real_analysis/oscillation.py +151 -0
  26. funcprops/real_analysis/piecewise.py +174 -0
  27. funcprops/real_analysis/range.py +552 -0
  28. funcprops/real_analysis/sign.py +561 -0
  29. funcprops/real_analysis/smoothness.py +268 -0
  30. funcprops/real_analysis/special_functions.py +515 -0
  31. funcprops/real_analysis/structural_properties.py +558 -0
  32. funcprops/real_analysis/zeros.py +327 -0
  33. funcprops/registry.py +274 -0
  34. funcprops/results.py +239 -0
  35. funcprops/singularities.py +1767 -0
  36. funcprops/stratification.py +494 -0
  37. funcprops/sympy_compare.py +63 -0
  38. funcprops/transcendental/__init__.py +140 -0
  39. funcprops/transcendental/algebraize.py +201 -0
  40. funcprops/transcendental/analytic.py +261 -0
  41. funcprops/transcendental/complex_roots.py +542 -0
  42. funcprops/transcendental/exp_fourier.py +221 -0
  43. funcprops/transcendental/extrema.py +821 -0
  44. funcprops/transcendental/periodic.py +222 -0
  45. funcprops/transcendental/proof.py +104 -0
  46. funcprops/transcendental/real_interval.py +128 -0
  47. funcprops/transcendental/real_sign.py +117 -0
  48. funcprops/transcendental/reduce.py +288 -0
  49. funcprops/transcendental/roots.py +1380 -0
  50. funcprops/transcendental/zero_geometry.py +1197 -0
  51. funcprops/verification.py +410 -0
  52. funcprops-0.1.0.dist-info/METADATA +208 -0
  53. funcprops-0.1.0.dist-info/RECORD +56 -0
  54. funcprops-0.1.0.dist-info/WHEEL +5 -0
  55. funcprops-0.1.0.dist-info/licenses/LICENSE +674 -0
  56. 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