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,552 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sympy as sp
|
|
4
|
+
from sympy import S
|
|
5
|
+
|
|
6
|
+
from .. import _semialg
|
|
7
|
+
from ..engine import evaluate_property
|
|
8
|
+
from ..exceptions import POLYNOMIAL_ERRORS, SYMBOLIC_QUERY_ERRORS
|
|
9
|
+
from ..results import ConditionalValue, PropertyResult
|
|
10
|
+
from ..stratification import (
|
|
11
|
+
evaluate_parameter_strata,
|
|
12
|
+
normalize_cases,
|
|
13
|
+
semantic_condition,
|
|
14
|
+
)
|
|
15
|
+
from ..transcendental.algebraize import algebraize
|
|
16
|
+
from .common import (
|
|
17
|
+
combine_natural_domain_condition,
|
|
18
|
+
condition_to_set,
|
|
19
|
+
is_full_natural_domain,
|
|
20
|
+
set_to_condition,
|
|
21
|
+
)
|
|
22
|
+
from .domain import function_domain
|
|
23
|
+
from .monotonic_certificates import monotonic_partition, range_from_monotonic_partition
|
|
24
|
+
from .special_functions import (
|
|
25
|
+
parameter_guard,
|
|
26
|
+
positive_axis_range_strata,
|
|
27
|
+
special_function_range_strata,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _lambda_condition(value, y):
|
|
32
|
+
"""Instantiate a one-argument catalogue condition at ``y``."""
|
|
33
|
+
if isinstance(value, sp.Lambda) and len(value.variables) == 1:
|
|
34
|
+
return sp.sympify(value.expr).xreplace({value.variables[0]: y})
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _refine_interval_attainment(expr, var, cond, y):
|
|
39
|
+
s = condition_to_set(cond, y)
|
|
40
|
+
if not isinstance(s, sp.Interval):
|
|
41
|
+
return cond
|
|
42
|
+
natural = function_domain(expr, var)
|
|
43
|
+
ds = condition_to_set(natural, var)
|
|
44
|
+
if ds is None:
|
|
45
|
+
return cond
|
|
46
|
+
left_open = s.left_open
|
|
47
|
+
right_open = s.right_open
|
|
48
|
+
if s.start not in (S.NegativeInfinity, S.Infinity):
|
|
49
|
+
try:
|
|
50
|
+
sol = sp.solveset(sp.Eq(expr, s.start), var, domain=ds)
|
|
51
|
+
if sol is S.EmptySet:
|
|
52
|
+
left_open = True
|
|
53
|
+
elif isinstance(sol, (sp.FiniteSet, sp.ImageSet, sp.Union)):
|
|
54
|
+
left_open = False
|
|
55
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
56
|
+
pass
|
|
57
|
+
if s.end not in (S.NegativeInfinity, S.Infinity):
|
|
58
|
+
try:
|
|
59
|
+
sol = sp.solveset(sp.Eq(expr, s.end), var, domain=ds)
|
|
60
|
+
if sol is S.EmptySet:
|
|
61
|
+
right_open = True
|
|
62
|
+
elif isinstance(sol, (sp.FiniteSet, sp.ImageSet, sp.Union)):
|
|
63
|
+
right_open = False
|
|
64
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
65
|
+
pass
|
|
66
|
+
return set_to_condition(
|
|
67
|
+
sp.Interval(s.start, s.end, left_open=left_open, right_open=right_open), y
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _registry_range(expr, var, y):
|
|
72
|
+
r = evaluate_property(expr, "Range", var=var, mode="real")
|
|
73
|
+
pguard = parameter_guard(expr, var, "Range")
|
|
74
|
+
if r.primary is not None:
|
|
75
|
+
cond = _lambda_condition(r.primary, y)
|
|
76
|
+
if cond is None:
|
|
77
|
+
return None
|
|
78
|
+
cond = _refine_interval_attainment(expr, var, cond, y)
|
|
79
|
+
if pguard is S.true:
|
|
80
|
+
return cond
|
|
81
|
+
return normalize_cases(
|
|
82
|
+
(ConditionalValue(cond, pguard),), source="registry-range-stratification"
|
|
83
|
+
)
|
|
84
|
+
if r.cases:
|
|
85
|
+
cases = []
|
|
86
|
+
for c in r.cases:
|
|
87
|
+
cond = _lambda_condition(c.value, y)
|
|
88
|
+
if cond is not None:
|
|
89
|
+
cond = _refine_interval_attainment(expr, var, cond, y)
|
|
90
|
+
cases.append(
|
|
91
|
+
ConditionalValue(
|
|
92
|
+
cond, sp.And(semantic_condition(c.condition), pguard)
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
if cases:
|
|
96
|
+
return normalize_cases(cases, source="registry-range-stratification")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _monotone_image_interval(outer, X, inner_set):
|
|
101
|
+
from ..results import Monotonicity
|
|
102
|
+
from .monotonicity import function_monotonicity
|
|
103
|
+
|
|
104
|
+
dom = set_to_condition(inner_set, X)
|
|
105
|
+
m = function_monotonicity(outer, X, domain=dom)
|
|
106
|
+
inc = m in (Monotonicity.STRICTLY_INCREASING, Monotonicity.NONDECREASING)
|
|
107
|
+
dec = m in (Monotonicity.STRICTLY_DECREASING, Monotonicity.NONINCREASING)
|
|
108
|
+
if not (inc or dec):
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def image_endpoint(v, is_left, is_open):
|
|
112
|
+
if v is S.NegativeInfinity:
|
|
113
|
+
return sp.limit(outer, X, -sp.oo)
|
|
114
|
+
if v is S.Infinity:
|
|
115
|
+
return sp.limit(outer, X, sp.oo)
|
|
116
|
+
if is_open:
|
|
117
|
+
try:
|
|
118
|
+
return sp.limit(outer, X, v, dir="+" if is_left else "-")
|
|
119
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
120
|
+
pass
|
|
121
|
+
return sp.simplify(outer.subs(X, v))
|
|
122
|
+
|
|
123
|
+
a = image_endpoint(inner_set.start, True, inner_set.left_open)
|
|
124
|
+
b = image_endpoint(inner_set.end, False, inner_set.right_open)
|
|
125
|
+
if inc:
|
|
126
|
+
lo, hi = a, b
|
|
127
|
+
lo_open = inner_set.left_open or inner_set.start in (
|
|
128
|
+
S.NegativeInfinity,
|
|
129
|
+
S.Infinity,
|
|
130
|
+
)
|
|
131
|
+
hi_open = inner_set.right_open or inner_set.end in (
|
|
132
|
+
S.NegativeInfinity,
|
|
133
|
+
S.Infinity,
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
lo, hi = b, a
|
|
137
|
+
lo_open = inner_set.right_open or inner_set.end in (
|
|
138
|
+
S.NegativeInfinity,
|
|
139
|
+
S.Infinity,
|
|
140
|
+
)
|
|
141
|
+
hi_open = inner_set.left_open or inner_set.start in (
|
|
142
|
+
S.NegativeInfinity,
|
|
143
|
+
S.Infinity,
|
|
144
|
+
)
|
|
145
|
+
if lo is S.NegativeInfinity:
|
|
146
|
+
lo_open = True
|
|
147
|
+
if hi is S.Infinity:
|
|
148
|
+
hi_open = True
|
|
149
|
+
return sp.Interval(lo, hi, left_open=lo_open, right_open=hi_open)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _composition_range(expr, var, y, domain_condition):
|
|
153
|
+
varying = [(i, a) for i, a in enumerate(getattr(expr, "args", ())) if a.has(var)]
|
|
154
|
+
if len(varying) != 1 or expr.func in (sp.Add, sp.Mul, sp.Pow, sp.Piecewise):
|
|
155
|
+
return None
|
|
156
|
+
i, inner = varying[0]
|
|
157
|
+
# A primitive f(x) is not a nontrivial composition. Recursing through
|
|
158
|
+
# function_range(x, ...) and then asking monotonicity of the same primitive
|
|
159
|
+
# can manufacture bogus empty/False images for oscillatory functions.
|
|
160
|
+
if inner == var:
|
|
161
|
+
return None
|
|
162
|
+
u = sp.Dummy("u", real=True)
|
|
163
|
+
rc = function_range(inner, var, u, constraints=domain_condition)
|
|
164
|
+
if isinstance(rc, PropertyResult):
|
|
165
|
+
return None
|
|
166
|
+
inner_set = condition_to_set(rc, u)
|
|
167
|
+
if not isinstance(inner_set, sp.Interval):
|
|
168
|
+
return None
|
|
169
|
+
X = sp.Dummy("X", real=True)
|
|
170
|
+
args = list(expr.args)
|
|
171
|
+
args[i] = X
|
|
172
|
+
outer = expr.func(*args)
|
|
173
|
+
out = _monotone_image_interval(outer, X, inner_set)
|
|
174
|
+
return set_to_condition(out, y) if out is not None else None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _scale_interval(s, A, B, sign):
|
|
178
|
+
def finite_image(v):
|
|
179
|
+
if v is S.NegativeInfinity:
|
|
180
|
+
return S.NegativeInfinity if sign > 0 else S.Infinity
|
|
181
|
+
if v is S.Infinity:
|
|
182
|
+
return S.Infinity if sign > 0 else S.NegativeInfinity
|
|
183
|
+
return sp.simplify(A * v + B)
|
|
184
|
+
|
|
185
|
+
if sign > 0:
|
|
186
|
+
return sp.Interval(
|
|
187
|
+
finite_image(s.start),
|
|
188
|
+
finite_image(s.end),
|
|
189
|
+
left_open=s.left_open,
|
|
190
|
+
right_open=s.right_open,
|
|
191
|
+
)
|
|
192
|
+
return sp.Interval(
|
|
193
|
+
finite_image(s.end),
|
|
194
|
+
finite_image(s.start),
|
|
195
|
+
left_open=s.right_open,
|
|
196
|
+
right_open=s.left_open,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _affine_transform(expr, var, y, domain):
|
|
201
|
+
"""Transform an exact inner range through ``A*g(x) + B``.
|
|
202
|
+
|
|
203
|
+
Parameter-dependent coefficient signs are preserved as conditional strata
|
|
204
|
+
instead of choosing an orientation heuristically.
|
|
205
|
+
"""
|
|
206
|
+
# Identify A*g(x)+B where A and B are independent of the input variable.
|
|
207
|
+
terms = sp.Add.make_args(sp.expand(expr))
|
|
208
|
+
const = sum((t for t in terms if not t.has(var)), S.Zero)
|
|
209
|
+
dep = sp.simplify(expr - const)
|
|
210
|
+
coeff, rest = dep.as_independent(var, as_Add=False)
|
|
211
|
+
if coeff.has(var) or not rest.has(var) or rest == expr:
|
|
212
|
+
return None
|
|
213
|
+
u = sp.Dummy("u", real=True)
|
|
214
|
+
inner = function_range(rest, var, u, constraints=domain)
|
|
215
|
+
|
|
216
|
+
def transform(cond, extra=S.true):
|
|
217
|
+
ss = condition_to_set(cond, u)
|
|
218
|
+
if not isinstance(ss, sp.Interval):
|
|
219
|
+
return None
|
|
220
|
+
A = sp.sympify(coeff)
|
|
221
|
+
B = sp.sympify(const)
|
|
222
|
+
if A.is_positive:
|
|
223
|
+
return set_to_condition(_scale_interval(ss, A, B, 1), y)
|
|
224
|
+
if A.is_negative:
|
|
225
|
+
return set_to_condition(_scale_interval(ss, A, B, -1), y)
|
|
226
|
+
if A.is_zero:
|
|
227
|
+
return sp.Eq(y, B)
|
|
228
|
+
return normalize_cases(
|
|
229
|
+
(
|
|
230
|
+
ConditionalValue(
|
|
231
|
+
set_to_condition(_scale_interval(ss, A, B, 1), y),
|
|
232
|
+
sp.And(extra, A > 0),
|
|
233
|
+
),
|
|
234
|
+
ConditionalValue(sp.Eq(y, B), sp.And(extra, sp.Eq(A, 0))),
|
|
235
|
+
ConditionalValue(
|
|
236
|
+
set_to_condition(_scale_interval(ss, A, B, -1), y),
|
|
237
|
+
sp.And(extra, A < 0),
|
|
238
|
+
),
|
|
239
|
+
),
|
|
240
|
+
source="affine-range-stratification",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if isinstance(inner, PropertyResult):
|
|
244
|
+
cases = []
|
|
245
|
+
for c in inner.cases:
|
|
246
|
+
tr = transform(c.value, c.condition)
|
|
247
|
+
if isinstance(tr, PropertyResult):
|
|
248
|
+
cases.extend(tr.cases)
|
|
249
|
+
elif tr is not None:
|
|
250
|
+
cases.append(ConditionalValue(tr, c.condition))
|
|
251
|
+
return (
|
|
252
|
+
normalize_cases(cases, source="affine-range-stratification")
|
|
253
|
+
if cases
|
|
254
|
+
else None
|
|
255
|
+
)
|
|
256
|
+
return transform(inner)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _parameterized_elementary_range(expr, var, y, domain_condition):
|
|
260
|
+
"""High-value exact parameter strata not handled by generic calculus."""
|
|
261
|
+
ds = condition_to_set(domain_condition, var)
|
|
262
|
+
try:
|
|
263
|
+
positive_axis = (
|
|
264
|
+
isinstance(ds, sp.Interval)
|
|
265
|
+
and ds.start == 0
|
|
266
|
+
and ds.left_open
|
|
267
|
+
and ds.end is S.Infinity
|
|
268
|
+
) or sp.simplify_logic(
|
|
269
|
+
sp.Implies(semantic_condition(domain_condition), var > 0)
|
|
270
|
+
) is S.true
|
|
271
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
272
|
+
positive_axis = False
|
|
273
|
+
# x**a on x>0: every nonzero real exponent maps onto (0,oo), while
|
|
274
|
+
# a=0 is the singleton 1. This must precede global catalogue ranges.
|
|
275
|
+
if positive_axis and expr.func is sp.Pow and expr.base == var:
|
|
276
|
+
A = expr.exp
|
|
277
|
+
if A.free_symbols:
|
|
278
|
+
real_guard = S.true if A.is_real is True else sp.Contains(A, S.Reals)
|
|
279
|
+
return normalize_cases(
|
|
280
|
+
(
|
|
281
|
+
ConditionalValue(y > 0, sp.And(real_guard, sp.Ne(A, 0))),
|
|
282
|
+
ConditionalValue(sp.Eq(y, 1), sp.Eq(A, 0)),
|
|
283
|
+
),
|
|
284
|
+
source="positive-power-range-stratification",
|
|
285
|
+
)
|
|
286
|
+
if ds is not S.Reals:
|
|
287
|
+
return None
|
|
288
|
+
# exp(a*x+b): nonzero real slope maps R bijectively to (0,oo); zero
|
|
289
|
+
# slope is the singleton exp(b).
|
|
290
|
+
if expr.func is sp.exp:
|
|
291
|
+
try:
|
|
292
|
+
p = sp.Poly(sp.expand(expr.args[0]), var)
|
|
293
|
+
except POLYNOMIAL_ERRORS:
|
|
294
|
+
p = None
|
|
295
|
+
if p is not None and p.degree() <= 1:
|
|
296
|
+
A = p.coeff_monomial(var)
|
|
297
|
+
B = p.nth(0)
|
|
298
|
+
if A.free_symbols and A.is_real is True and B.is_real is True:
|
|
299
|
+
return normalize_cases(
|
|
300
|
+
(
|
|
301
|
+
ConditionalValue(y > 0, sp.Ne(A, 0)),
|
|
302
|
+
ConditionalValue(sp.Eq(y, sp.exp(B)), sp.Eq(A, 0)),
|
|
303
|
+
),
|
|
304
|
+
source="exp-affine-range-stratification",
|
|
305
|
+
)
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _sympy_range(expr, var, y, domain_condition):
|
|
310
|
+
ds = condition_to_set(domain_condition, var)
|
|
311
|
+
if ds is None:
|
|
312
|
+
return None
|
|
313
|
+
try:
|
|
314
|
+
rs = sp.calculus.util.function_range(expr, var, ds)
|
|
315
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
316
|
+
return None
|
|
317
|
+
if isinstance(rs, sp.Set) and not rs.has(var):
|
|
318
|
+
return set_to_condition(rs, y)
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _early_range_stage(expr, var, y, domain_condition, constraints):
|
|
323
|
+
"""Run cheap parameter, catalogue, and positive-axis range rules."""
|
|
324
|
+
pr = _parameterized_elementary_range(expr, var, y, domain_condition)
|
|
325
|
+
if pr is not None:
|
|
326
|
+
return pr
|
|
327
|
+
sf_domain = domain_condition if constraints is True else sp.sympify(constraints)
|
|
328
|
+
sfacts = special_function_range_strata(expr, var, y, sf_domain)
|
|
329
|
+
if sfacts:
|
|
330
|
+
out = normalize_cases(
|
|
331
|
+
(ConditionalValue(v, g) for v, g, *_ in sfacts),
|
|
332
|
+
source="dlmf-special-function-range-stratification",
|
|
333
|
+
evidence=tuple(src for *_, src in sfacts),
|
|
334
|
+
)
|
|
335
|
+
return out.primary if out.primary is not None else out
|
|
336
|
+
ds0 = condition_to_set(domain_condition, var)
|
|
337
|
+
try:
|
|
338
|
+
positive_axis = (
|
|
339
|
+
isinstance(ds0, sp.Interval)
|
|
340
|
+
and ds0.start == 0
|
|
341
|
+
and ds0.left_open
|
|
342
|
+
and ds0.end is S.Infinity
|
|
343
|
+
) or sp.simplify_logic(
|
|
344
|
+
sp.Implies(semantic_condition(domain_condition), var > 0)
|
|
345
|
+
) is S.true
|
|
346
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
347
|
+
positive_axis = False
|
|
348
|
+
if positive_axis:
|
|
349
|
+
facts = positive_axis_range_strata(expr, var, y)
|
|
350
|
+
if facts:
|
|
351
|
+
out = normalize_cases(
|
|
352
|
+
(ConditionalValue(v, g) for v, g, *_ in facts),
|
|
353
|
+
source="special-function-range-stratification",
|
|
354
|
+
evidence=tuple(src for *_, src in facts),
|
|
355
|
+
)
|
|
356
|
+
return out.primary if out.primary is not None else out
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _semialgebraic_range_stage(expr, var, y, domain_condition, method, kwargs):
|
|
361
|
+
"""Run structural then exact semialgebraic range analysis."""
|
|
362
|
+
if not _semialg.eligible_problem(expr, var, domain=domain_condition):
|
|
363
|
+
return None
|
|
364
|
+
structural = _affine_transform(expr, var, y, domain_condition)
|
|
365
|
+
if structural is not None:
|
|
366
|
+
return (
|
|
367
|
+
structural
|
|
368
|
+
if isinstance(structural, PropertyResult)
|
|
369
|
+
else sp.simplify(structural)
|
|
370
|
+
)
|
|
371
|
+
try:
|
|
372
|
+
return _semialg.function_range(
|
|
373
|
+
expr,
|
|
374
|
+
var,
|
|
375
|
+
y,
|
|
376
|
+
constraints=domain_condition,
|
|
377
|
+
method="qe" if method == "auto" else method,
|
|
378
|
+
**kwargs,
|
|
379
|
+
)
|
|
380
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def function_range(
|
|
385
|
+
expr,
|
|
386
|
+
var,
|
|
387
|
+
y,
|
|
388
|
+
*,
|
|
389
|
+
constraints=True,
|
|
390
|
+
method="auto",
|
|
391
|
+
partition=None,
|
|
392
|
+
_domain_ready=False,
|
|
393
|
+
**kwargs,
|
|
394
|
+
):
|
|
395
|
+
"""Return the real range as a Boolean condition in ``y``.
|
|
396
|
+
|
|
397
|
+
Strategy: primitive lookup, structural transformations, SymPy's univariate
|
|
398
|
+
calculus algorithms, and semialg for the semialgebraic subset.
|
|
399
|
+
"""
|
|
400
|
+
expr = sp.sympify(expr)
|
|
401
|
+
var = sp.sympify(var)
|
|
402
|
+
y = sp.sympify(y)
|
|
403
|
+
natural = function_domain(expr, var)
|
|
404
|
+
domain_condition = (
|
|
405
|
+
sp.sympify(constraints)
|
|
406
|
+
if _domain_ready
|
|
407
|
+
else combine_natural_domain_condition(natural, constraints, var)
|
|
408
|
+
)
|
|
409
|
+
if not _domain_ready:
|
|
410
|
+
split = evaluate_parameter_strata(
|
|
411
|
+
domain_condition,
|
|
412
|
+
var,
|
|
413
|
+
lambda branch_domain: function_range(
|
|
414
|
+
expr,
|
|
415
|
+
var,
|
|
416
|
+
y,
|
|
417
|
+
constraints=branch_domain,
|
|
418
|
+
method=method,
|
|
419
|
+
partition=None,
|
|
420
|
+
_domain_ready=True,
|
|
421
|
+
**kwargs,
|
|
422
|
+
),
|
|
423
|
+
source="range-parameter-stratification",
|
|
424
|
+
)
|
|
425
|
+
if split is not None:
|
|
426
|
+
return split
|
|
427
|
+
if not expr.has(var):
|
|
428
|
+
return sp.Eq(y, expr)
|
|
429
|
+
|
|
430
|
+
early = _early_range_stage(expr, var, y, domain_condition, constraints)
|
|
431
|
+
if early is not None:
|
|
432
|
+
return early
|
|
433
|
+
|
|
434
|
+
# Preserve parameter strata before delegating semialgebraic elimination.
|
|
435
|
+
# A backend QE call may quantify/treat free coefficients in a way that
|
|
436
|
+
# collapses guarded image cases (for example a*x on x>0) to a tautology.
|
|
437
|
+
backend = _semialgebraic_range_stage(expr, var, y, domain_condition, method, kwargs)
|
|
438
|
+
if backend is not None:
|
|
439
|
+
return backend
|
|
440
|
+
|
|
441
|
+
# Exact transcendental algebraization can turn the range problem into a
|
|
442
|
+
# semialgebraic one without changing the output variable. For example,
|
|
443
|
+
# exp(2*x)-3*exp(x)+2 becomes u**2-3*u+2 on u>0.
|
|
444
|
+
ar = algebraize(expr, var)
|
|
445
|
+
if ar.changed and ar.exact and len(ar.substitutions) == 1:
|
|
446
|
+
u, _ = ar.substitutions[0]
|
|
447
|
+
# Preserve openness of the auxiliary image (notably u=exp(x)>0).
|
|
448
|
+
# SymPy's univariate calculus is tried first because it represents
|
|
449
|
+
# endpoint attainment explicitly; semialg remains the exact fallback.
|
|
450
|
+
try:
|
|
451
|
+
us = condition_to_set(ar.constraints, u)
|
|
452
|
+
if us is not None:
|
|
453
|
+
rs = sp.calculus.util.function_range(ar.expression, u, us)
|
|
454
|
+
if isinstance(rs, sp.Set):
|
|
455
|
+
return set_to_condition(rs, y)
|
|
456
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
457
|
+
pass
|
|
458
|
+
try:
|
|
459
|
+
raw = _semialg.function_range(
|
|
460
|
+
ar.expression,
|
|
461
|
+
u,
|
|
462
|
+
y,
|
|
463
|
+
constraints=ar.constraints,
|
|
464
|
+
method="qe" if method == "auto" else method,
|
|
465
|
+
**kwargs,
|
|
466
|
+
)
|
|
467
|
+
# Refine finite interval endpoints against the actual auxiliary
|
|
468
|
+
# domain so a backend closure approximation cannot turn > into >=.
|
|
469
|
+
rs = condition_to_set(raw, y)
|
|
470
|
+
us = condition_to_set(ar.constraints, u)
|
|
471
|
+
if isinstance(rs, sp.Interval) and us is not None:
|
|
472
|
+
lo_open, hi_open = rs.left_open, rs.right_open
|
|
473
|
+
if rs.start not in (S.NegativeInfinity, S.Infinity):
|
|
474
|
+
sol = sp.solveset(sp.Eq(ar.expression, rs.start), u, domain=us)
|
|
475
|
+
if sol is S.EmptySet:
|
|
476
|
+
lo_open = True
|
|
477
|
+
if rs.end not in (S.NegativeInfinity, S.Infinity):
|
|
478
|
+
sol = sp.solveset(sp.Eq(ar.expression, rs.end), u, domain=us)
|
|
479
|
+
if sol is S.EmptySet:
|
|
480
|
+
hi_open = True
|
|
481
|
+
return set_to_condition(
|
|
482
|
+
sp.Interval(
|
|
483
|
+
rs.start, rs.end, left_open=lo_open, right_open=hi_open
|
|
484
|
+
),
|
|
485
|
+
y,
|
|
486
|
+
)
|
|
487
|
+
return raw
|
|
488
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
489
|
+
pass
|
|
490
|
+
|
|
491
|
+
# Monotone composition can turn a known inner interval into the exact
|
|
492
|
+
# outer range (e.g. exp(sin(x)) -> [exp(-1), exp(1)]).
|
|
493
|
+
r = _composition_range(expr, var, y, domain_condition)
|
|
494
|
+
if r is not None:
|
|
495
|
+
return r if isinstance(r, PropertyResult) else sp.simplify(r)
|
|
496
|
+
|
|
497
|
+
# Primitive catalogue ranges are exact and cheapest.
|
|
498
|
+
if constraints is True or is_full_natural_domain(domain_condition, natural):
|
|
499
|
+
r = _registry_range(expr, var, y)
|
|
500
|
+
if r is not None:
|
|
501
|
+
return r if isinstance(r, PropertyResult) else sp.simplify(r)
|
|
502
|
+
|
|
503
|
+
ds = condition_to_set(domain_condition, var)
|
|
504
|
+
part = (
|
|
505
|
+
partition
|
|
506
|
+
if partition is not None and getattr(partition, "domain", None) == ds
|
|
507
|
+
else None
|
|
508
|
+
)
|
|
509
|
+
# Indexed special-function geometry is cheap to recognize and expensive to
|
|
510
|
+
# rediscover through generic calculus, so build the shared partition first
|
|
511
|
+
# for those families. For generic expressions keep SymPy's successful fast
|
|
512
|
+
# path ahead of a potentially redundant derivative-root solve.
|
|
513
|
+
if isinstance(ds, sp.Interval):
|
|
514
|
+
try:
|
|
515
|
+
from ..transcendental.zero_geometry import zero_geometry
|
|
516
|
+
|
|
517
|
+
if part is None and zero_geometry(expr, var, derivative_order=1, domain=ds):
|
|
518
|
+
part = monotonic_partition(expr, var, domain=ds)
|
|
519
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
520
|
+
pass
|
|
521
|
+
if part is not None:
|
|
522
|
+
prange = range_from_monotonic_partition(expr, var, part)
|
|
523
|
+
if prange.complete and isinstance(prange.image, sp.Set):
|
|
524
|
+
return set_to_condition(prange.image, y)
|
|
525
|
+
|
|
526
|
+
r = _sympy_range(expr, var, y, domain_condition)
|
|
527
|
+
if r is not None:
|
|
528
|
+
return sp.simplify(r)
|
|
529
|
+
|
|
530
|
+
if part is None and isinstance(ds, sp.Interval):
|
|
531
|
+
part = monotonic_partition(expr, var, domain=ds)
|
|
532
|
+
prange = range_from_monotonic_partition(expr, var, part)
|
|
533
|
+
if prange.complete and isinstance(prange.image, sp.Set):
|
|
534
|
+
return set_to_condition(prange.image, y)
|
|
535
|
+
|
|
536
|
+
# Indexed extrema consume the same partition certificate, avoiding a second
|
|
537
|
+
# derivative-zero geometry pass.
|
|
538
|
+
try:
|
|
539
|
+
from ..transcendental.extrema import symbolic_extremum_range
|
|
540
|
+
|
|
541
|
+
er = symbolic_extremum_range(
|
|
542
|
+
expr,
|
|
543
|
+
var,
|
|
544
|
+
ds if isinstance(ds, sp.Interval) else domain_condition,
|
|
545
|
+
partition=part,
|
|
546
|
+
)
|
|
547
|
+
if er is not None and er.complete:
|
|
548
|
+
cond = er.as_condition(y)
|
|
549
|
+
return cond if cond is not None else er
|
|
550
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
551
|
+
pass
|
|
552
|
+
return sp.Function("UnknownRange")(expr, var, y)
|