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,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sympy as sp
|
|
4
|
+
from sympy import S
|
|
5
|
+
|
|
6
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
7
|
+
from ..results import Boundedness, ConditionalValue, PropertyResult
|
|
8
|
+
from ..stratification import normalize_cases
|
|
9
|
+
from .common import combine_natural_domain_condition, condition_to_set
|
|
10
|
+
from .domain import function_domain
|
|
11
|
+
from .piecewise import (
|
|
12
|
+
combine_piecewise_boundedness,
|
|
13
|
+
piecewise_branches,
|
|
14
|
+
)
|
|
15
|
+
from .range import function_range
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _boundedness_from_range_condition(cond, y):
|
|
19
|
+
"""Infer two-sided boundedness from an exact range representation.
|
|
20
|
+
|
|
21
|
+
Indexed extremum ranges already carry exact endpoint objects, so boundedness
|
|
22
|
+
depends only on whether either endpoint is infinite; no envelope reduction
|
|
23
|
+
is required here.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
from ..transcendental.extrema import ExtremumRange
|
|
27
|
+
|
|
28
|
+
if isinstance(cond, ExtremumRange):
|
|
29
|
+
if not cond.complete:
|
|
30
|
+
return Boundedness.UNKNOWN
|
|
31
|
+
lo, hi = cond.lower, cond.upper
|
|
32
|
+
below = lo is not S.NegativeInfinity and lo != S.NegativeInfinity
|
|
33
|
+
above = hi is not S.Infinity and hi != S.Infinity
|
|
34
|
+
if below and above:
|
|
35
|
+
return Boundedness.BOUNDED
|
|
36
|
+
if below:
|
|
37
|
+
return Boundedness.BOUNDED_BELOW
|
|
38
|
+
if above:
|
|
39
|
+
return Boundedness.BOUNDED_ABOVE
|
|
40
|
+
return Boundedness.UNBOUNDED
|
|
41
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
42
|
+
pass
|
|
43
|
+
# Parameter-dependent endpoints often cannot be converted by SymPy's
|
|
44
|
+
# ``as_set`` (e.g. 0 < y < gamma(a)). Infer one-/two-sided boundedness
|
|
45
|
+
# directly from exact relational structure before falling back to Sets.
|
|
46
|
+
c = sp.sympify(cond)
|
|
47
|
+
rels = c.args if isinstance(c, sp.And) else (c,)
|
|
48
|
+
lower = False
|
|
49
|
+
upper = False
|
|
50
|
+
for r in rels:
|
|
51
|
+
if not isinstance(r, sp.core.relational.Relational):
|
|
52
|
+
continue
|
|
53
|
+
try:
|
|
54
|
+
if r.lhs == y and not r.rhs.has(y):
|
|
55
|
+
if isinstance(r, (sp.StrictGreaterThan, sp.GreaterThan)):
|
|
56
|
+
lower = True
|
|
57
|
+
elif isinstance(r, (sp.StrictLessThan, sp.LessThan)):
|
|
58
|
+
upper = True
|
|
59
|
+
elif r.rhs == y and not r.lhs.has(y):
|
|
60
|
+
if isinstance(r, (sp.StrictLessThan, sp.LessThan)):
|
|
61
|
+
lower = True
|
|
62
|
+
elif isinstance(r, (sp.StrictGreaterThan, sp.GreaterThan)):
|
|
63
|
+
upper = True
|
|
64
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
65
|
+
pass
|
|
66
|
+
if lower and upper:
|
|
67
|
+
return Boundedness.BOUNDED
|
|
68
|
+
if lower:
|
|
69
|
+
return Boundedness.BOUNDED_BELOW
|
|
70
|
+
if upper:
|
|
71
|
+
return Boundedness.BOUNDED_ABOVE
|
|
72
|
+
s = condition_to_set(cond, y)
|
|
73
|
+
if s is None:
|
|
74
|
+
return Boundedness.UNKNOWN
|
|
75
|
+
if s is S.EmptySet:
|
|
76
|
+
# The empty-domain convention is vacuously bounded in both directions.
|
|
77
|
+
return Boundedness.BOUNDED
|
|
78
|
+
try:
|
|
79
|
+
lo = s.inf
|
|
80
|
+
hi = s.sup
|
|
81
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
82
|
+
return Boundedness.UNKNOWN
|
|
83
|
+
below = lo != S.NegativeInfinity
|
|
84
|
+
above = hi != S.Infinity
|
|
85
|
+
if below and above:
|
|
86
|
+
return Boundedness.BOUNDED
|
|
87
|
+
if below:
|
|
88
|
+
return Boundedness.BOUNDED_BELOW
|
|
89
|
+
if above:
|
|
90
|
+
return Boundedness.BOUNDED_ABOVE
|
|
91
|
+
return Boundedness.UNBOUNDED
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _structural_boundedness(expr, var, domain):
|
|
95
|
+
"""Apply cheap closure rules before invoking a full range computation."""
|
|
96
|
+
if not expr.has(var):
|
|
97
|
+
return Boundedness.BOUNDED
|
|
98
|
+
if expr.func in (sp.sin, sp.cos, sp.tanh, sp.atan):
|
|
99
|
+
return Boundedness.BOUNDED
|
|
100
|
+
if isinstance(expr, sp.Piecewise):
|
|
101
|
+
branches = piecewise_branches(expr, var, domain)
|
|
102
|
+
if branches:
|
|
103
|
+
values = [
|
|
104
|
+
function_boundedness(branch.expression, var, domain=branch.condition)
|
|
105
|
+
for branch in branches
|
|
106
|
+
]
|
|
107
|
+
return combine_piecewise_boundedness(values)
|
|
108
|
+
if expr.func is sp.Abs:
|
|
109
|
+
inner = function_boundedness(expr.args[0], var, domain=domain)
|
|
110
|
+
if inner is Boundedness.BOUNDED:
|
|
111
|
+
return Boundedness.BOUNDED
|
|
112
|
+
return Boundedness.BOUNDED_BELOW
|
|
113
|
+
if expr.func is sp.exp:
|
|
114
|
+
inner = function_boundedness(expr.args[0], var, domain=domain)
|
|
115
|
+
if inner in (Boundedness.BOUNDED, Boundedness.BOUNDED_ABOVE):
|
|
116
|
+
return Boundedness.BOUNDED
|
|
117
|
+
return Boundedness.BOUNDED_BELOW
|
|
118
|
+
if expr.is_Add:
|
|
119
|
+
values = [function_boundedness(a, var, domain=domain) for a in expr.args]
|
|
120
|
+
if not any(isinstance(v, PropertyResult) for v in values):
|
|
121
|
+
upper = {Boundedness.BOUNDED, Boundedness.BOUNDED_ABOVE}
|
|
122
|
+
lower = {Boundedness.BOUNDED, Boundedness.BOUNDED_BELOW}
|
|
123
|
+
above = all(v in upper for v in values)
|
|
124
|
+
below = all(v in lower for v in values)
|
|
125
|
+
if above and below:
|
|
126
|
+
return Boundedness.BOUNDED
|
|
127
|
+
if above:
|
|
128
|
+
return Boundedness.BOUNDED_ABOVE
|
|
129
|
+
if below:
|
|
130
|
+
return Boundedness.BOUNDED_BELOW
|
|
131
|
+
if expr.is_Mul:
|
|
132
|
+
values = [function_boundedness(a, var, domain=domain) for a in expr.args]
|
|
133
|
+
if values and all(v is Boundedness.BOUNDED for v in values):
|
|
134
|
+
return Boundedness.BOUNDED
|
|
135
|
+
return Boundedness.UNKNOWN
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def function_boundedness(expr, var, *, domain=True):
|
|
139
|
+
expr = sp.sympify(expr)
|
|
140
|
+
var = sp.sympify(var)
|
|
141
|
+
y = sp.Dummy("_y", real=True)
|
|
142
|
+
structural = _structural_boundedness(expr, var, domain)
|
|
143
|
+
if structural is not Boundedness.UNKNOWN:
|
|
144
|
+
return structural
|
|
145
|
+
natural = function_domain(expr, var)
|
|
146
|
+
# function_range owns natural-domain intersection. Passing an already
|
|
147
|
+
# combined mixed parameter/domain formula can obscure a simple user domain
|
|
148
|
+
# (e.g. x>=0) and prevent endpoint-aware special-function rules from firing.
|
|
149
|
+
constraints = combine_natural_domain_condition(natural, domain, var)
|
|
150
|
+
try:
|
|
151
|
+
cond = function_range(expr, var, y, constraints=constraints)
|
|
152
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
153
|
+
return Boundedness.UNKNOWN
|
|
154
|
+
if isinstance(cond, PropertyResult):
|
|
155
|
+
out = normalize_cases(
|
|
156
|
+
(
|
|
157
|
+
ConditionalValue(
|
|
158
|
+
_boundedness_from_range_condition(c.value, y), c.condition
|
|
159
|
+
)
|
|
160
|
+
for c in cond.cases
|
|
161
|
+
),
|
|
162
|
+
primary=_boundedness_from_range_condition(cond.primary, y)
|
|
163
|
+
if cond.primary is not None
|
|
164
|
+
else None,
|
|
165
|
+
source="range-boundedness-stratification",
|
|
166
|
+
)
|
|
167
|
+
return out if out.cases else Boundedness.UNKNOWN
|
|
168
|
+
return _boundedness_from_range_condition(cond, y)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sympy as sp
|
|
4
|
+
from sympy import S
|
|
5
|
+
|
|
6
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_semialgebraic_expression(expr: sp.Expr, var: sp.Symbol) -> bool:
|
|
10
|
+
"""Conservative structural test for the subset handled by semialg.
|
|
11
|
+
|
|
12
|
+
Algebraic powers with rational exponent, Abs/Min/Max/Piecewise and ordinary
|
|
13
|
+
polynomial/rational arithmetic are accepted. Unknown Function heads are not.
|
|
14
|
+
"""
|
|
15
|
+
expr = sp.sympify(expr)
|
|
16
|
+
if not expr.has(var):
|
|
17
|
+
return True
|
|
18
|
+
if expr.is_Atom:
|
|
19
|
+
return expr == var or expr.is_number or expr.is_Symbol
|
|
20
|
+
if expr.func in (sp.Add, sp.Mul):
|
|
21
|
+
return all(is_semialgebraic_expression(a, var) for a in expr.args)
|
|
22
|
+
if expr.func is sp.Pow:
|
|
23
|
+
b, e = expr.args
|
|
24
|
+
return bool(e.is_Rational) and is_semialgebraic_expression(b, var)
|
|
25
|
+
if expr.func in (sp.Abs, sp.Min, sp.Max):
|
|
26
|
+
return all(is_semialgebraic_expression(a, var) for a in expr.args)
|
|
27
|
+
if isinstance(expr, sp.Piecewise):
|
|
28
|
+
return all(
|
|
29
|
+
is_semialgebraic_expression(v, var) and is_semialgebraic_condition(c, var)
|
|
30
|
+
for v, c in expr.args
|
|
31
|
+
)
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_semialgebraic_condition(cond, var):
|
|
36
|
+
cond = sp.sympify(cond)
|
|
37
|
+
if cond in (S.true, S.false):
|
|
38
|
+
return True
|
|
39
|
+
if isinstance(cond, (sp.And, sp.Or)):
|
|
40
|
+
return all(is_semialgebraic_condition(a, var) for a in cond.args)
|
|
41
|
+
if isinstance(cond, sp.Not):
|
|
42
|
+
return is_semialgebraic_condition(cond.args[0], var)
|
|
43
|
+
if isinstance(cond, sp.core.relational.Relational):
|
|
44
|
+
return is_semialgebraic_expression(
|
|
45
|
+
cond.lhs, var
|
|
46
|
+
) and is_semialgebraic_expression(cond.rhs, var)
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def combine_domain(user_domain, natural_domain):
|
|
51
|
+
if user_domain is None or user_domain is True or user_domain is S.true:
|
|
52
|
+
return sp.sympify(natural_domain)
|
|
53
|
+
if isinstance(user_domain, sp.Set):
|
|
54
|
+
return sp.And(sp.Contains(sp.Symbol("_fp_dummy"), user_domain), natural_domain)
|
|
55
|
+
return sp.simplify(sp.And(sp.sympify(user_domain), sp.sympify(natural_domain)))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def condition_to_set(condition, var):
|
|
59
|
+
"""Convert ordinary univariate Boolean conditions to a SymPy Set.
|
|
60
|
+
|
|
61
|
+
SymPy's ``Relational.as_set()`` can refuse parameterized endpoints such as
|
|
62
|
+
``x >= A`` even though the set is exactly ``[A, oo)`` with respect to x.
|
|
63
|
+
Handle those one-variable affine endpoint relations structurally first.
|
|
64
|
+
"""
|
|
65
|
+
condition = sp.sympify(condition)
|
|
66
|
+
var = sp.sympify(var)
|
|
67
|
+
if condition is S.true:
|
|
68
|
+
return S.Reals
|
|
69
|
+
if condition is S.false:
|
|
70
|
+
return S.EmptySet
|
|
71
|
+
if (
|
|
72
|
+
isinstance(condition, sp.Contains)
|
|
73
|
+
and condition.args[0] == var
|
|
74
|
+
and isinstance(condition.args[1], sp.Set)
|
|
75
|
+
):
|
|
76
|
+
return condition.args[1]
|
|
77
|
+
if isinstance(condition, sp.And):
|
|
78
|
+
pieces = [condition_to_set(a, var) for a in condition.args]
|
|
79
|
+
if all(isinstance(q, sp.Set) for q in pieces):
|
|
80
|
+
return sp.Intersection(*pieces)
|
|
81
|
+
if isinstance(condition, sp.Or):
|
|
82
|
+
pieces = [condition_to_set(a, var) for a in condition.args]
|
|
83
|
+
if all(isinstance(q, sp.Set) for q in pieces):
|
|
84
|
+
return sp.Union(*pieces)
|
|
85
|
+
if isinstance(condition, sp.core.relational.Relational):
|
|
86
|
+
lhs, rhs = condition.lhs, condition.rhs
|
|
87
|
+
if lhs == var and not rhs.has(var):
|
|
88
|
+
if isinstance(condition, sp.GreaterThan):
|
|
89
|
+
return sp.Interval(rhs, sp.oo)
|
|
90
|
+
if isinstance(condition, sp.StrictGreaterThan):
|
|
91
|
+
return sp.Interval.open(rhs, sp.oo)
|
|
92
|
+
if isinstance(condition, sp.LessThan):
|
|
93
|
+
return sp.Interval(-sp.oo, rhs)
|
|
94
|
+
if isinstance(condition, sp.StrictLessThan):
|
|
95
|
+
return sp.Interval.open(-sp.oo, rhs)
|
|
96
|
+
if isinstance(condition, sp.Equality):
|
|
97
|
+
return sp.FiniteSet(rhs)
|
|
98
|
+
if rhs == var and not lhs.has(var):
|
|
99
|
+
if isinstance(condition, sp.GreaterThan):
|
|
100
|
+
return sp.Interval(-sp.oo, lhs)
|
|
101
|
+
if isinstance(condition, sp.StrictGreaterThan):
|
|
102
|
+
return sp.Interval.open(-sp.oo, lhs)
|
|
103
|
+
if isinstance(condition, sp.LessThan):
|
|
104
|
+
return sp.Interval(lhs, sp.oo)
|
|
105
|
+
if isinstance(condition, sp.StrictLessThan):
|
|
106
|
+
return sp.Interval.open(lhs, sp.oo)
|
|
107
|
+
if isinstance(condition, sp.Equality):
|
|
108
|
+
return sp.FiniteSet(lhs)
|
|
109
|
+
try:
|
|
110
|
+
if isinstance(
|
|
111
|
+
condition, (sp.logic.boolalg.Boolean, sp.core.relational.Relational)
|
|
112
|
+
):
|
|
113
|
+
return condition.as_set()
|
|
114
|
+
except (AttributeError, *SYMBOLIC_QUERY_ERRORS):
|
|
115
|
+
# Semantic catalogue predicates are BooleanFunction subclasses without
|
|
116
|
+
# SymPy's optional _eval_as_set implementation.
|
|
117
|
+
pass
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def set_to_condition(set_, y):
|
|
122
|
+
"""Convert a real Set to a Boolean membership condition in ``y``."""
|
|
123
|
+
if set_ == S.Reals:
|
|
124
|
+
return S.true
|
|
125
|
+
if set_ == S.EmptySet:
|
|
126
|
+
return S.false
|
|
127
|
+
if isinstance(set_, sp.Interval):
|
|
128
|
+
pieces = []
|
|
129
|
+
if set_.start is not S.NegativeInfinity:
|
|
130
|
+
pieces.append(y > set_.start if set_.left_open else y >= set_.start)
|
|
131
|
+
if set_.end is not S.Infinity:
|
|
132
|
+
pieces.append(y < set_.end if set_.right_open else y <= set_.end)
|
|
133
|
+
return sp.And(*pieces) if pieces else S.true
|
|
134
|
+
if isinstance(set_, sp.FiniteSet):
|
|
135
|
+
return sp.Or(*(sp.Eq(y, v) for v in set_)) if set_ else S.false
|
|
136
|
+
if isinstance(set_, sp.Union):
|
|
137
|
+
return sp.Or(*(set_to_condition(a, y) for a in set_.args))
|
|
138
|
+
if isinstance(set_, sp.Complement):
|
|
139
|
+
a, b = set_.args
|
|
140
|
+
return sp.And(set_to_condition(a, y), sp.Not(set_to_condition(b, y)))
|
|
141
|
+
return sp.Contains(y, set_)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def interval_components(condition, var):
|
|
145
|
+
s = condition_to_set(condition, var)
|
|
146
|
+
if s is None:
|
|
147
|
+
return None
|
|
148
|
+
if isinstance(s, sp.Interval):
|
|
149
|
+
return (s,)
|
|
150
|
+
if isinstance(s, sp.Union) and all(isinstance(a, sp.Interval) for a in s.args):
|
|
151
|
+
return tuple(s.args)
|
|
152
|
+
if s == S.Reals:
|
|
153
|
+
return (sp.Interval(-sp.oo, sp.oo),)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def is_full_natural_domain(requested_domain, natural_domain):
|
|
158
|
+
if (
|
|
159
|
+
requested_domain is True
|
|
160
|
+
or requested_domain is S.true
|
|
161
|
+
or requested_domain is None
|
|
162
|
+
):
|
|
163
|
+
return True
|
|
164
|
+
try:
|
|
165
|
+
# Custom catalogue Boolean predicates must be lowered before generic
|
|
166
|
+
# SymPy Boolean simplification; otherwise unknown predicate heads can
|
|
167
|
+
# be simplified unsafely or merely syntactically.
|
|
168
|
+
from ..stratification import semantic_condition
|
|
169
|
+
|
|
170
|
+
req = semantic_condition(requested_domain)
|
|
171
|
+
nat = semantic_condition(natural_domain)
|
|
172
|
+
eq = sp.simplify(sp.simplify_logic(sp.Equivalent(req, nat)))
|
|
173
|
+
return eq is S.true
|
|
174
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def exact_order_real_points(points):
|
|
179
|
+
"""Order exact real points only when every comparison is certified.
|
|
180
|
+
|
|
181
|
+
Returns ``None`` rather than using structural or floating-point ordering
|
|
182
|
+
when a pair cannot be compared exactly. This helper is shared by sign,
|
|
183
|
+
monotonic-partition, extrema, and inverse reasoning.
|
|
184
|
+
"""
|
|
185
|
+
out = []
|
|
186
|
+
for q in points:
|
|
187
|
+
q = sp.sympify(q)
|
|
188
|
+
placed = False
|
|
189
|
+
for i, r in enumerate(out):
|
|
190
|
+
d = sp.simplify(q - r)
|
|
191
|
+
if d.is_negative is True:
|
|
192
|
+
out.insert(i, q)
|
|
193
|
+
placed = True
|
|
194
|
+
break
|
|
195
|
+
if d.is_positive is True:
|
|
196
|
+
continue
|
|
197
|
+
if d.is_zero is True:
|
|
198
|
+
placed = True
|
|
199
|
+
break
|
|
200
|
+
return None
|
|
201
|
+
if not placed:
|
|
202
|
+
out.append(q)
|
|
203
|
+
return tuple(out)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def combine_natural_domain_condition(natural_domain, user_domain, var):
|
|
207
|
+
"""Combine a natural-domain result with a requested real domain safely.
|
|
208
|
+
|
|
209
|
+
A ``PropertyResult`` means the natural domain was not established as one
|
|
210
|
+
Boolean condition. In that case preserve the explicit user restriction,
|
|
211
|
+
or use all reals when no restriction was supplied; never pass the result
|
|
212
|
+
object itself into SymPy Boolean simplification.
|
|
213
|
+
"""
|
|
214
|
+
var = sp.sympify(var)
|
|
215
|
+
nat = None if hasattr(natural_domain, "primary") else sp.sympify(natural_domain)
|
|
216
|
+
if user_domain is True or user_domain is None or user_domain is S.true:
|
|
217
|
+
return S.true if nat is None else nat
|
|
218
|
+
if isinstance(user_domain, sp.Set):
|
|
219
|
+
usr = sp.Contains(var, user_domain)
|
|
220
|
+
else:
|
|
221
|
+
usr = sp.sympify(user_domain)
|
|
222
|
+
if nat is None:
|
|
223
|
+
return usr
|
|
224
|
+
try:
|
|
225
|
+
return sp.simplify(sp.And(nat, usr))
|
|
226
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
227
|
+
return sp.And(nat, usr)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def normalize_variables(var):
|
|
231
|
+
"""Return a canonical tuple of symbolic variables.
|
|
232
|
+
|
|
233
|
+
Public APIs that accept either one variable or a variable collection must
|
|
234
|
+
not sympify a tuple and then test ``expr.has(tuple)``: SymPy treats the
|
|
235
|
+
resulting Tuple as one atomic expression. This helper centralizes the
|
|
236
|
+
scalar/collection distinction without changing scalar call sites.
|
|
237
|
+
"""
|
|
238
|
+
if isinstance(var, (tuple, list, sp.Tuple)):
|
|
239
|
+
vv = tuple(sp.sympify(v) for v in var)
|
|
240
|
+
else:
|
|
241
|
+
vv = (sp.sympify(var),)
|
|
242
|
+
if not vv:
|
|
243
|
+
raise ValueError("at least one variable is required")
|
|
244
|
+
return vv
|