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,867 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
import sympy as sp
|
|
8
|
+
from sympy import S
|
|
9
|
+
|
|
10
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
11
|
+
from ..results import Monotonicity, TruthValue
|
|
12
|
+
from .common import condition_to_set, exact_order_real_points
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class MonotonicDecomposition:
|
|
17
|
+
expression: sp.Basic
|
|
18
|
+
variable: sp.Symbol
|
|
19
|
+
inner: sp.Basic
|
|
20
|
+
outer: sp.Lambda
|
|
21
|
+
direction: int
|
|
22
|
+
condition: sp.Basic = S.true
|
|
23
|
+
inverse: sp.Lambda | None = None
|
|
24
|
+
strict: bool = True
|
|
25
|
+
continuous: bool = True
|
|
26
|
+
source: str = "structural-monotonic-decomposition"
|
|
27
|
+
|
|
28
|
+
def reconstruct(self):
|
|
29
|
+
return sp.simplify(self.outer(self.inner))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class MonotonicInterval:
|
|
34
|
+
interval: sp.Set
|
|
35
|
+
direction: Monotonicity
|
|
36
|
+
left_limit: sp.Basic | None = None
|
|
37
|
+
right_limit: sp.Basic | None = None
|
|
38
|
+
strict: bool = True
|
|
39
|
+
continuous: bool = True
|
|
40
|
+
left_attained: bool | None = None
|
|
41
|
+
right_attained: bool | None = None
|
|
42
|
+
evidence: tuple = ()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def increasing(self):
|
|
46
|
+
return self.direction in (
|
|
47
|
+
Monotonicity.STRICTLY_INCREASING,
|
|
48
|
+
Monotonicity.NONDECREASING,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def decreasing(self):
|
|
53
|
+
return self.direction in (
|
|
54
|
+
Monotonicity.STRICTLY_DECREASING,
|
|
55
|
+
Monotonicity.NONINCREASING,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class IndexedMonotonicPartition:
|
|
61
|
+
domain: sp.Set
|
|
62
|
+
boundary_geometry: tuple
|
|
63
|
+
# Empty means the geometry proves alternation but not its phase. This is
|
|
64
|
+
# deliberately different from assuming that the leftmost cell increases.
|
|
65
|
+
direction_pattern: tuple[Monotonicity, ...] = ()
|
|
66
|
+
complete: bool = False
|
|
67
|
+
first_direction: Monotonicity | None = None
|
|
68
|
+
source: str = "indexed-derivative-zero-geometry"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class MonotonicPartition:
|
|
73
|
+
domain: sp.Set
|
|
74
|
+
cells: tuple[MonotonicInterval, ...] = ()
|
|
75
|
+
indexed: IndexedMonotonicPartition | None = None
|
|
76
|
+
complete: bool = False
|
|
77
|
+
source: str = "monotonic-partition"
|
|
78
|
+
derivative: sp.Basic | None = None
|
|
79
|
+
critical_points: tuple = ()
|
|
80
|
+
property_decomposition: object = None
|
|
81
|
+
critical_points_complete: bool = False
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def finite(self):
|
|
85
|
+
return self.indexed is None
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def direction_changes(self):
|
|
89
|
+
vals = [c.direction for c in self.cells]
|
|
90
|
+
n = 0
|
|
91
|
+
for a, b in itertools.pairwise(vals):
|
|
92
|
+
if a != b:
|
|
93
|
+
n += 1
|
|
94
|
+
return n
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def has_indexed_oscillation(self):
|
|
98
|
+
return bool(
|
|
99
|
+
self.indexed is not None
|
|
100
|
+
and self.indexed.complete
|
|
101
|
+
and self.indexed.boundary_geometry
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def same_strict_direction(self):
|
|
105
|
+
if not self.complete or not self.cells:
|
|
106
|
+
return None
|
|
107
|
+
dirs = {c.direction for c in self.cells}
|
|
108
|
+
if dirs == {Monotonicity.STRICTLY_INCREASING}:
|
|
109
|
+
return Monotonicity.STRICTLY_INCREASING
|
|
110
|
+
if dirs == {Monotonicity.STRICTLY_DECREASING}:
|
|
111
|
+
return Monotonicity.STRICTLY_DECREASING
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True)
|
|
116
|
+
class NonMonotonicityWitness:
|
|
117
|
+
points: tuple
|
|
118
|
+
values: tuple
|
|
119
|
+
exact: bool
|
|
120
|
+
source: str = "exact-sample-ordering-witness"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class InverseSolutionCertificate:
|
|
125
|
+
equation: sp.Basic
|
|
126
|
+
domain: sp.Set
|
|
127
|
+
solutions: sp.Set
|
|
128
|
+
complete: bool
|
|
129
|
+
partition: MonotonicPartition | None = None
|
|
130
|
+
source: str = "monotonic-partition-inverse"
|
|
131
|
+
evidence: tuple = ()
|
|
132
|
+
isolated_intervals: tuple = ()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class PartitionRange:
|
|
137
|
+
image: sp.Set | None
|
|
138
|
+
complete: bool
|
|
139
|
+
partition: MonotonicPartition
|
|
140
|
+
source: str = "monotonic-partition-range"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _monotone_head(expr, X):
|
|
144
|
+
"""Return (direction, condition, inverse) for high-value primitive heads."""
|
|
145
|
+
f = expr.func
|
|
146
|
+
args = expr.args
|
|
147
|
+
slot = sp.Dummy("u", real=True)
|
|
148
|
+
if len(args) != 1 or args[0] != X:
|
|
149
|
+
return None
|
|
150
|
+
if f in (sp.exp, sp.log, sp.sinh, sp.tanh, sp.atan, sp.asinh, sp.erf, sp.erfi):
|
|
151
|
+
cond = X > 0 if f is sp.log else S.true
|
|
152
|
+
inv = {
|
|
153
|
+
sp.exp: sp.log,
|
|
154
|
+
sp.log: sp.exp,
|
|
155
|
+
sp.sinh: sp.asinh,
|
|
156
|
+
sp.tanh: sp.atanh,
|
|
157
|
+
sp.atan: sp.tan,
|
|
158
|
+
sp.asinh: sp.sinh,
|
|
159
|
+
sp.erf: sp.erfinv,
|
|
160
|
+
}.get(f)
|
|
161
|
+
invlam = sp.Lambda(slot, inv(slot)) if inv is not None else None
|
|
162
|
+
return 1, cond, invlam
|
|
163
|
+
if f in (sp.acos, sp.erfc):
|
|
164
|
+
cond = sp.And(X >= -1, X <= 1) if f is sp.acos else S.true
|
|
165
|
+
inv = sp.cos if f is sp.acos else getattr(sp, "erfcinv", None)
|
|
166
|
+
return -1, cond, (sp.Lambda(slot, inv(slot)) if inv else None)
|
|
167
|
+
if f is sp.asin:
|
|
168
|
+
return 1, sp.And(X >= -1, X <= 1), sp.Lambda(slot, sp.sin(slot))
|
|
169
|
+
if f is sp.acosh:
|
|
170
|
+
return 1, X >= 1, sp.Lambda(slot, sp.cosh(slot))
|
|
171
|
+
if f is sp.atanh:
|
|
172
|
+
return 1, sp.And(X > -1, X < 1), sp.Lambda(slot, sp.tanh(slot))
|
|
173
|
+
if f is sp.LambertW and len(args) == 1:
|
|
174
|
+
return 1, X >= -1 / sp.E, sp.Lambda(slot, slot * sp.exp(slot))
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def monotonic_decomposition(expr, var, *, strict=True, continuous=True):
|
|
179
|
+
"""Find ``expr = F(inner)`` with a certified monotone outer ``F``."""
|
|
180
|
+
expr = sp.sympify(expr)
|
|
181
|
+
var = sp.sympify(var)
|
|
182
|
+
if not expr.has(var):
|
|
183
|
+
return MonotonicDecomposition(
|
|
184
|
+
expr,
|
|
185
|
+
var,
|
|
186
|
+
S.Zero,
|
|
187
|
+
sp.Lambda(sp.Dummy("u"), expr),
|
|
188
|
+
1,
|
|
189
|
+
S.true,
|
|
190
|
+
None,
|
|
191
|
+
False,
|
|
192
|
+
True,
|
|
193
|
+
"constant",
|
|
194
|
+
)
|
|
195
|
+
u = sp.Dummy("u", real=True)
|
|
196
|
+
if expr.is_Add:
|
|
197
|
+
c, dep = expr.as_independent(var, as_Add=True)
|
|
198
|
+
if c != 0 and dep != expr:
|
|
199
|
+
return MonotonicDecomposition(
|
|
200
|
+
expr,
|
|
201
|
+
var,
|
|
202
|
+
dep,
|
|
203
|
+
sp.Lambda(u, u + c),
|
|
204
|
+
1,
|
|
205
|
+
S.true,
|
|
206
|
+
sp.Lambda(u, u - c),
|
|
207
|
+
strict,
|
|
208
|
+
continuous,
|
|
209
|
+
"affine-shift",
|
|
210
|
+
)
|
|
211
|
+
if expr.is_Mul:
|
|
212
|
+
c, dep = expr.as_independent(var, as_Add=False)
|
|
213
|
+
if dep != expr and c.is_real is True and c.is_zero is not True:
|
|
214
|
+
if c.is_positive is True:
|
|
215
|
+
return MonotonicDecomposition(
|
|
216
|
+
expr,
|
|
217
|
+
var,
|
|
218
|
+
dep,
|
|
219
|
+
sp.Lambda(u, c * u),
|
|
220
|
+
1,
|
|
221
|
+
S.true,
|
|
222
|
+
sp.Lambda(u, u / c),
|
|
223
|
+
strict,
|
|
224
|
+
continuous,
|
|
225
|
+
"positive-scale",
|
|
226
|
+
)
|
|
227
|
+
if c.is_negative is True:
|
|
228
|
+
return MonotonicDecomposition(
|
|
229
|
+
expr,
|
|
230
|
+
var,
|
|
231
|
+
dep,
|
|
232
|
+
sp.Lambda(u, c * u),
|
|
233
|
+
-1,
|
|
234
|
+
S.true,
|
|
235
|
+
sp.Lambda(u, u / c),
|
|
236
|
+
strict,
|
|
237
|
+
continuous,
|
|
238
|
+
"negative-scale",
|
|
239
|
+
)
|
|
240
|
+
if isinstance(expr, sp.Pow):
|
|
241
|
+
base, exponent = expr.args
|
|
242
|
+
if base.has(var) and not exponent.has(var):
|
|
243
|
+
if exponent.is_integer and exponent.is_positive:
|
|
244
|
+
if exponent.is_odd:
|
|
245
|
+
return MonotonicDecomposition(
|
|
246
|
+
expr,
|
|
247
|
+
var,
|
|
248
|
+
base,
|
|
249
|
+
sp.Lambda(u, u**exponent),
|
|
250
|
+
1,
|
|
251
|
+
S.true,
|
|
252
|
+
sp.Lambda(u, sp.real_root(u, exponent)),
|
|
253
|
+
strict,
|
|
254
|
+
continuous,
|
|
255
|
+
"odd-positive-power",
|
|
256
|
+
)
|
|
257
|
+
if exponent.is_even and exponent != 2:
|
|
258
|
+
return MonotonicDecomposition(
|
|
259
|
+
expr,
|
|
260
|
+
var,
|
|
261
|
+
base**2,
|
|
262
|
+
sp.Lambda(u, u ** (exponent / 2)),
|
|
263
|
+
1,
|
|
264
|
+
S.true,
|
|
265
|
+
None,
|
|
266
|
+
strict,
|
|
267
|
+
continuous,
|
|
268
|
+
"even-power-square-reduction",
|
|
269
|
+
)
|
|
270
|
+
if exponent.is_Rational:
|
|
271
|
+
d = 1 if exponent.is_positive else -1
|
|
272
|
+
cond = base >= 0 if exponent.is_positive else base > 0
|
|
273
|
+
return MonotonicDecomposition(
|
|
274
|
+
expr,
|
|
275
|
+
var,
|
|
276
|
+
base,
|
|
277
|
+
sp.Lambda(u, u**exponent),
|
|
278
|
+
d,
|
|
279
|
+
cond,
|
|
280
|
+
None,
|
|
281
|
+
strict,
|
|
282
|
+
continuous,
|
|
283
|
+
"rational-power",
|
|
284
|
+
)
|
|
285
|
+
if (
|
|
286
|
+
not base.has(var)
|
|
287
|
+
and exponent.has(var)
|
|
288
|
+
and base.is_positive is True
|
|
289
|
+
and base != 1
|
|
290
|
+
):
|
|
291
|
+
d = 1 if (base - 1).is_positive else (-1 if (base - 1).is_negative else 0)
|
|
292
|
+
if d:
|
|
293
|
+
return MonotonicDecomposition(
|
|
294
|
+
expr,
|
|
295
|
+
var,
|
|
296
|
+
exponent,
|
|
297
|
+
sp.Lambda(u, base**u),
|
|
298
|
+
d,
|
|
299
|
+
S.true,
|
|
300
|
+
sp.Lambda(u, sp.log(u, base)),
|
|
301
|
+
strict,
|
|
302
|
+
continuous,
|
|
303
|
+
"positive-base-exponential",
|
|
304
|
+
)
|
|
305
|
+
varying = [
|
|
306
|
+
(i, a)
|
|
307
|
+
for i, a in enumerate(getattr(expr, "args", ()))
|
|
308
|
+
if getattr(a, "has", lambda *_: False)(var)
|
|
309
|
+
]
|
|
310
|
+
if len(varying) == 1 and expr.func not in (sp.Add, sp.Mul, sp.Pow, sp.Piecewise):
|
|
311
|
+
i, inner = varying[0]
|
|
312
|
+
if inner != var:
|
|
313
|
+
X = sp.Dummy("X", real=True)
|
|
314
|
+
aa = list(expr.args)
|
|
315
|
+
aa[i] = X
|
|
316
|
+
outer_expr = expr.func(*aa)
|
|
317
|
+
info = _monotone_head(outer_expr, X)
|
|
318
|
+
if info:
|
|
319
|
+
d, cond, inv = info
|
|
320
|
+
return MonotonicDecomposition(
|
|
321
|
+
expr,
|
|
322
|
+
var,
|
|
323
|
+
inner,
|
|
324
|
+
sp.Lambda(X, outer_expr),
|
|
325
|
+
d,
|
|
326
|
+
cond.xreplace({X: inner}),
|
|
327
|
+
inv,
|
|
328
|
+
strict,
|
|
329
|
+
continuous,
|
|
330
|
+
"elementary-monotone-head",
|
|
331
|
+
)
|
|
332
|
+
try:
|
|
333
|
+
from .monotonicity import _direct_registry
|
|
334
|
+
|
|
335
|
+
m = _direct_registry(outer_expr, X)
|
|
336
|
+
inc = (Monotonicity.STRICTLY_INCREASING, Monotonicity.NONDECREASING)
|
|
337
|
+
dec = (Monotonicity.STRICTLY_DECREASING, Monotonicity.NONINCREASING)
|
|
338
|
+
if m in inc + dec:
|
|
339
|
+
d = 1 if m in inc else -1
|
|
340
|
+
from .domain import function_domain
|
|
341
|
+
|
|
342
|
+
cond = function_domain(outer_expr, X)
|
|
343
|
+
cond = (
|
|
344
|
+
cond.xreplace({X: inner})
|
|
345
|
+
if isinstance(cond, sp.Basic)
|
|
346
|
+
else S.true
|
|
347
|
+
)
|
|
348
|
+
return MonotonicDecomposition(
|
|
349
|
+
expr,
|
|
350
|
+
var,
|
|
351
|
+
inner,
|
|
352
|
+
sp.Lambda(X, outer_expr),
|
|
353
|
+
d,
|
|
354
|
+
cond,
|
|
355
|
+
None,
|
|
356
|
+
m in (inc[0], dec[0]),
|
|
357
|
+
continuous,
|
|
358
|
+
"registry-monotone-head",
|
|
359
|
+
)
|
|
360
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
361
|
+
pass
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _endpoint_limit(expr, var, v, left, open_, domain=S.true, assumptions=S.true):
|
|
366
|
+
from ..proof_cache import property_proof_context
|
|
367
|
+
|
|
368
|
+
return property_proof_context(
|
|
369
|
+
expr, var, domain=domain, assumptions=assumptions
|
|
370
|
+
).endpoint_limit(v, direction=("+" if left else "-"), attained=not open_)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _phase_for_indexed(expr, var, ds, pd, derivative=None):
|
|
374
|
+
"""Determine an indexed alternating phase only when an exact theorem/sample does."""
|
|
375
|
+
# For an infinite family the zero geometry itself proves alternation but
|
|
376
|
+
# usually does not identify which sign occurs on the leftmost unbounded
|
|
377
|
+
# cell. A finite endpoint cell can sometimes be sampled exactly.
|
|
378
|
+
if not isinstance(ds, sp.Interval) or not pd.boundary_geometry:
|
|
379
|
+
return None
|
|
380
|
+
try:
|
|
381
|
+
from ..proof_cache import cached_derivative
|
|
382
|
+
|
|
383
|
+
d = (
|
|
384
|
+
sp.factor(derivative)
|
|
385
|
+
if derivative is not None
|
|
386
|
+
else cached_derivative(expr, var, order=1, factored=True)
|
|
387
|
+
)
|
|
388
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
389
|
+
return None
|
|
390
|
+
# Airy phase is known at x=0 exactly. The cell immediately left of zero
|
|
391
|
+
# therefore fixes the parity without numerical root evaluation.
|
|
392
|
+
if expr.func in (sp.airyai, sp.airybi) and ds.end == 0:
|
|
393
|
+
val = sp.simplify(d.subs(var, 0))
|
|
394
|
+
if val.is_positive is True:
|
|
395
|
+
return Monotonicity.STRICTLY_INCREASING
|
|
396
|
+
if val.is_negative is True:
|
|
397
|
+
return Monotonicity.STRICTLY_DECREASING
|
|
398
|
+
# Otherwise do not guess a phase from structural zero ordering.
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@lru_cache(maxsize=512)
|
|
403
|
+
def _monotonic_partition_on_set(expr, var, domain_set):
|
|
404
|
+
"""Build an exact monotonic partition on an already-normalized real set.
|
|
405
|
+
|
|
406
|
+
The routine prefers derivative zero geometry, then falls back to finite
|
|
407
|
+
critical-point analysis. Completeness is retained explicitly so consumers do
|
|
408
|
+
not mistake a partial decomposition for a global proof.
|
|
409
|
+
"""
|
|
410
|
+
ds = domain_set
|
|
411
|
+
expr = sp.sympify(expr)
|
|
412
|
+
var = sp.sympify(var)
|
|
413
|
+
if not isinstance(ds, sp.Set):
|
|
414
|
+
return MonotonicPartition(S.Reals, complete=False, source="unknown-domain")
|
|
415
|
+
derivative = None
|
|
416
|
+
try:
|
|
417
|
+
from ..proof_cache import property_proof_context
|
|
418
|
+
|
|
419
|
+
derivative = property_proof_context(expr, var, domain=ds).derivative(
|
|
420
|
+
1, factored=True
|
|
421
|
+
)
|
|
422
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
423
|
+
pass
|
|
424
|
+
from ..transcendental.zero_geometry import property_interval_decomposition
|
|
425
|
+
|
|
426
|
+
pd = property_interval_decomposition(
|
|
427
|
+
expr,
|
|
428
|
+
var,
|
|
429
|
+
derivative_order=1,
|
|
430
|
+
domain=ds,
|
|
431
|
+
positive_value=Monotonicity.STRICTLY_INCREASING,
|
|
432
|
+
negative_value=Monotonicity.STRICTLY_DECREASING,
|
|
433
|
+
derivative=derivative,
|
|
434
|
+
)
|
|
435
|
+
if pd.cells:
|
|
436
|
+
cells = []
|
|
437
|
+
crit = ()
|
|
438
|
+
for g in pd.boundary_geometry:
|
|
439
|
+
if getattr(g, "family", None) == "finite-derivative-zeros" and isinstance(
|
|
440
|
+
getattr(g, "support", None), sp.FiniteSet
|
|
441
|
+
):
|
|
442
|
+
q = exact_order_real_points(tuple(g.support))
|
|
443
|
+
if q is not None:
|
|
444
|
+
crit = tuple(
|
|
445
|
+
z
|
|
446
|
+
for z in q
|
|
447
|
+
if not (isinstance(ds, sp.Interval) and z in (ds.start, ds.end))
|
|
448
|
+
)
|
|
449
|
+
break
|
|
450
|
+
for j, c in enumerate(pd.cells):
|
|
451
|
+
it = getattr(c, "interval", None) or getattr(c, "domain", None)
|
|
452
|
+
value = getattr(c, "value", None) or getattr(c, "property", None)
|
|
453
|
+
if not isinstance(it, sp.Interval) or value is None:
|
|
454
|
+
continue
|
|
455
|
+
left_outer = j == 0 and isinstance(ds, sp.Interval) and it.start == ds.start
|
|
456
|
+
right_outer = (
|
|
457
|
+
j == len(pd.cells) - 1
|
|
458
|
+
and isinstance(ds, sp.Interval)
|
|
459
|
+
and it.end == ds.end
|
|
460
|
+
)
|
|
461
|
+
la = (
|
|
462
|
+
(not ds.left_open)
|
|
463
|
+
if left_outer and isinstance(ds, sp.Interval)
|
|
464
|
+
else True
|
|
465
|
+
)
|
|
466
|
+
ra = (
|
|
467
|
+
(not ds.right_open)
|
|
468
|
+
if right_outer and isinstance(ds, sp.Interval)
|
|
469
|
+
else True
|
|
470
|
+
)
|
|
471
|
+
cells.append(
|
|
472
|
+
MonotonicInterval(
|
|
473
|
+
it,
|
|
474
|
+
value,
|
|
475
|
+
_endpoint_limit(expr, var, it.start, True, not la, ds),
|
|
476
|
+
_endpoint_limit(expr, var, it.end, False, not ra, ds),
|
|
477
|
+
True,
|
|
478
|
+
True,
|
|
479
|
+
la,
|
|
480
|
+
ra,
|
|
481
|
+
(c,),
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
crit_complete = bool(
|
|
485
|
+
pd.boundary_geometry
|
|
486
|
+
and any(
|
|
487
|
+
getattr(g, "family", None) == "finite-derivative-zeros"
|
|
488
|
+
and getattr(g, "complete_on_support", False)
|
|
489
|
+
for g in pd.boundary_geometry
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
return MonotonicPartition(
|
|
493
|
+
ds,
|
|
494
|
+
tuple(cells),
|
|
495
|
+
complete=bool(pd.complete and len(cells) == len(pd.cells)),
|
|
496
|
+
source="finite-derivative-zero-partition",
|
|
497
|
+
derivative=derivative,
|
|
498
|
+
critical_points=crit,
|
|
499
|
+
property_decomposition=pd,
|
|
500
|
+
critical_points_complete=crit_complete,
|
|
501
|
+
)
|
|
502
|
+
if pd.boundary_geometry and pd.alternating:
|
|
503
|
+
phase = _phase_for_indexed(expr, var, ds, pd, derivative)
|
|
504
|
+
pattern = (
|
|
505
|
+
()
|
|
506
|
+
if phase is None
|
|
507
|
+
else (
|
|
508
|
+
phase,
|
|
509
|
+
Monotonicity.STRICTLY_DECREASING
|
|
510
|
+
if phase is Monotonicity.STRICTLY_INCREASING
|
|
511
|
+
else Monotonicity.STRICTLY_INCREASING,
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
idx = IndexedMonotonicPartition(
|
|
515
|
+
ds, pd.boundary_geometry, pattern, pd.complete, phase
|
|
516
|
+
)
|
|
517
|
+
return MonotonicPartition(
|
|
518
|
+
ds,
|
|
519
|
+
indexed=idx,
|
|
520
|
+
complete=pd.complete,
|
|
521
|
+
source="indexed-derivative-zero-geometry",
|
|
522
|
+
derivative=derivative,
|
|
523
|
+
property_decomposition=pd,
|
|
524
|
+
critical_points_complete=False,
|
|
525
|
+
)
|
|
526
|
+
return MonotonicPartition(
|
|
527
|
+
ds,
|
|
528
|
+
complete=False,
|
|
529
|
+
derivative=derivative,
|
|
530
|
+
property_decomposition=pd,
|
|
531
|
+
critical_points_complete=False,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def monotonic_partition(expr, var, *, domain=True):
|
|
536
|
+
"""Return a cached exact finite or indexed partition into monotone cells."""
|
|
537
|
+
expr = sp.sympify(expr)
|
|
538
|
+
var = sp.sympify(var)
|
|
539
|
+
if isinstance(domain, sp.Set):
|
|
540
|
+
ds = domain
|
|
541
|
+
else:
|
|
542
|
+
if domain is True:
|
|
543
|
+
from .domain import function_domain
|
|
544
|
+
|
|
545
|
+
cond = function_domain(expr, var)
|
|
546
|
+
else:
|
|
547
|
+
cond = sp.sympify(domain)
|
|
548
|
+
ds = condition_to_set(cond, var)
|
|
549
|
+
if not isinstance(ds, sp.Set):
|
|
550
|
+
return MonotonicPartition(S.Reals, complete=False, source="unknown-domain")
|
|
551
|
+
try:
|
|
552
|
+
return _monotonic_partition_on_set(expr, var, ds)
|
|
553
|
+
except TypeError:
|
|
554
|
+
return _monotonic_partition_on_set.__wrapped__(expr, var, ds)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def clear_monotonic_partition_cache():
|
|
558
|
+
# Preserve the public spelling while clearing all proof primitives that can
|
|
559
|
+
# feed a partition; otherwise a "cleared" partition cache can immediately
|
|
560
|
+
# repopulate from stale lower-level proofs.
|
|
561
|
+
from ..proof_cache import clear_property_proof_cache
|
|
562
|
+
|
|
563
|
+
clear_property_proof_cache()
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _cell_image(cell: MonotonicInterval):
|
|
567
|
+
if not isinstance(cell.interval, sp.Interval):
|
|
568
|
+
return None
|
|
569
|
+
a, b = cell.left_limit, cell.right_limit
|
|
570
|
+
if a is None or b is None:
|
|
571
|
+
return None
|
|
572
|
+
if cell.increasing:
|
|
573
|
+
lo, hi = a, b
|
|
574
|
+
la, ua = cell.left_attained, cell.right_attained
|
|
575
|
+
elif cell.decreasing:
|
|
576
|
+
lo, hi = b, a
|
|
577
|
+
la, ua = cell.right_attained, cell.left_attained
|
|
578
|
+
else:
|
|
579
|
+
return None
|
|
580
|
+
if la is None or ua is None:
|
|
581
|
+
return None
|
|
582
|
+
if lo is S.NegativeInfinity:
|
|
583
|
+
la = False
|
|
584
|
+
if hi is S.Infinity:
|
|
585
|
+
ua = False
|
|
586
|
+
try:
|
|
587
|
+
return sp.Interval(lo, hi, left_open=not la, right_open=not ua)
|
|
588
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def range_from_monotonic_partition(expr, var, partition=None, *, domain=True):
|
|
593
|
+
"""Exact image set when a finite monotonic partition is complete."""
|
|
594
|
+
part = partition or monotonic_partition(expr, var, domain=domain)
|
|
595
|
+
if not part.complete or part.indexed is not None or not part.cells:
|
|
596
|
+
return PartitionRange(None, False, part)
|
|
597
|
+
images = []
|
|
598
|
+
for c in part.cells:
|
|
599
|
+
im = _cell_image(c)
|
|
600
|
+
if im is None:
|
|
601
|
+
return PartitionRange(None, False, part)
|
|
602
|
+
images.append(im)
|
|
603
|
+
try:
|
|
604
|
+
image = sp.Union(*images)
|
|
605
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
606
|
+
return PartitionRange(None, False, part)
|
|
607
|
+
return PartitionRange(image, True, part)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def injectivity_from_monotonic_partition(partition):
|
|
611
|
+
"""Three-valued injectivity conclusion derived only from a complete partition."""
|
|
612
|
+
if not isinstance(partition, MonotonicPartition) or not partition.complete:
|
|
613
|
+
return TruthValue.UNKNOWN
|
|
614
|
+
if partition.indexed is not None:
|
|
615
|
+
# Infinite simple alternating extrema force collisions on an interval.
|
|
616
|
+
return (
|
|
617
|
+
TruthValue.FALSE
|
|
618
|
+
if partition.indexed.boundary_geometry
|
|
619
|
+
else TruthValue.UNKNOWN
|
|
620
|
+
)
|
|
621
|
+
if not partition.cells:
|
|
622
|
+
return TruthValue.UNKNOWN
|
|
623
|
+
same = partition.same_strict_direction()
|
|
624
|
+
if same is not None:
|
|
625
|
+
return TruthValue.TRUE
|
|
626
|
+
# Direction reversal at an attained continuous interior critical point
|
|
627
|
+
# creates a strict local extremum and hence duplicate nearby values.
|
|
628
|
+
for a, b in zip(partition.cells, partition.cells[1:], strict=False):
|
|
629
|
+
if (
|
|
630
|
+
a.direction != b.direction
|
|
631
|
+
and a.continuous
|
|
632
|
+
and b.continuous
|
|
633
|
+
and a.right_attained
|
|
634
|
+
and b.left_attained
|
|
635
|
+
):
|
|
636
|
+
return TruthValue.FALSE
|
|
637
|
+
return TruthValue.UNKNOWN
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _certified_inverse_cell(expr, var, value, cell):
|
|
641
|
+
"""Resolve one strict monotonic cell exactly or by a rational isolating interval."""
|
|
642
|
+
from ..transcendental.real_sign import robust_real_sign
|
|
643
|
+
from ..transcendental.roots import CertifiedRootInterval, RootCertificate
|
|
644
|
+
|
|
645
|
+
interval = cell.interval
|
|
646
|
+
if not isinstance(interval, sp.Interval) or not cell.strict:
|
|
647
|
+
return None
|
|
648
|
+
left = None if cell.left_limit is None else sp.simplify(cell.left_limit - value)
|
|
649
|
+
right = None if cell.right_limit is None else sp.simplify(cell.right_limit - value)
|
|
650
|
+
if left is None or right is None:
|
|
651
|
+
return None
|
|
652
|
+
sl = robust_real_sign(left)
|
|
653
|
+
sr = robust_real_sign(right)
|
|
654
|
+
if sl is None or sr is None:
|
|
655
|
+
return None
|
|
656
|
+
exact = []
|
|
657
|
+
if sl == 0 and cell.left_attained:
|
|
658
|
+
exact.append(interval.start)
|
|
659
|
+
if sr == 0 and cell.right_attained:
|
|
660
|
+
exact.append(interval.end)
|
|
661
|
+
if exact:
|
|
662
|
+
return sp.FiniteSet(*exact), (), True
|
|
663
|
+
if sl == sr and sl in (-1, 1):
|
|
664
|
+
return S.EmptySet, (), True
|
|
665
|
+
if sl * sr != -1:
|
|
666
|
+
return None
|
|
667
|
+
if interval.start in (S.NegativeInfinity, S.Infinity) or interval.end in (
|
|
668
|
+
S.NegativeInfinity,
|
|
669
|
+
S.Infinity,
|
|
670
|
+
):
|
|
671
|
+
return None
|
|
672
|
+
|
|
673
|
+
h = sp.simplify(expr - value)
|
|
674
|
+
a, b = interval.start, interval.end
|
|
675
|
+
# Move to exact interior dyadic points when an endpoint is open or non-rational.
|
|
676
|
+
aa = a
|
|
677
|
+
bb = b
|
|
678
|
+
left_sign = sl
|
|
679
|
+
right_sign = sr
|
|
680
|
+
for level in range(4, 28):
|
|
681
|
+
if robust_real_sign(sp.simplify(h.subs(var, aa))) != left_sign:
|
|
682
|
+
aa = sp.simplify(a + (b - a) / (2**level))
|
|
683
|
+
if robust_real_sign(sp.simplify(h.subs(var, bb))) != right_sign:
|
|
684
|
+
bb = sp.simplify(b - (b - a) / (2**level))
|
|
685
|
+
if (
|
|
686
|
+
robust_real_sign(sp.simplify(h.subs(var, aa))) == left_sign
|
|
687
|
+
and robust_real_sign(sp.simplify(h.subs(var, bb))) == right_sign
|
|
688
|
+
):
|
|
689
|
+
break
|
|
690
|
+
else:
|
|
691
|
+
return None
|
|
692
|
+
for _ in range(32):
|
|
693
|
+
m = sp.simplify((aa + bb) / 2)
|
|
694
|
+
sm = robust_real_sign(sp.simplify(h.subs(var, m)))
|
|
695
|
+
if sm == 0:
|
|
696
|
+
return sp.FiniteSet(m), (), True
|
|
697
|
+
if sm not in (-1, 1):
|
|
698
|
+
break
|
|
699
|
+
if sm == left_sign:
|
|
700
|
+
aa = m
|
|
701
|
+
else:
|
|
702
|
+
bb = m
|
|
703
|
+
iv = sp.Interval(aa, bb)
|
|
704
|
+
cert = RootCertificate(
|
|
705
|
+
interval,
|
|
706
|
+
1,
|
|
707
|
+
"certified_monotonic_inverse",
|
|
708
|
+
(("target", value), ("endpoint_signs", sl, sr), ("isolating_interval", iv)),
|
|
709
|
+
True,
|
|
710
|
+
1,
|
|
711
|
+
)
|
|
712
|
+
return S.EmptySet, (CertifiedRootInterval(iv, cert),), True
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def solve_monotonic_inverse(expr, var, value, *, domain=True, partition=None):
|
|
716
|
+
"""Solve ``expr == value`` using a shared monotonic partition certificate.
|
|
717
|
+
|
|
718
|
+
Finite cells are solved independently, with uniqueness guaranteed on each
|
|
719
|
+
strict cell. Indexed partitions remain exact ``ConditionSet`` objects;
|
|
720
|
+
this routine never numerically truncates an infinite family.
|
|
721
|
+
"""
|
|
722
|
+
expr = sp.sympify(expr)
|
|
723
|
+
var = sp.sympify(var)
|
|
724
|
+
value = sp.sympify(value)
|
|
725
|
+
part = partition or monotonic_partition(expr, var, domain=domain)
|
|
726
|
+
eq = sp.Eq(expr, value)
|
|
727
|
+
if not part.complete:
|
|
728
|
+
ds = part.domain if isinstance(part.domain, sp.Set) else S.Reals
|
|
729
|
+
return InverseSolutionCertificate(
|
|
730
|
+
eq,
|
|
731
|
+
ds,
|
|
732
|
+
sp.ConditionSet(var, eq, ds),
|
|
733
|
+
False,
|
|
734
|
+
part,
|
|
735
|
+
source="incomplete-monotonic-partition",
|
|
736
|
+
)
|
|
737
|
+
if part.indexed is not None:
|
|
738
|
+
return InverseSolutionCertificate(
|
|
739
|
+
eq,
|
|
740
|
+
part.domain,
|
|
741
|
+
sp.ConditionSet(var, eq, part.domain),
|
|
742
|
+
False,
|
|
743
|
+
part,
|
|
744
|
+
source="indexed-monotonic-partition",
|
|
745
|
+
)
|
|
746
|
+
# Prefer one global exact solve; splitting by monotonic cells is a fallback,
|
|
747
|
+
# not a reason to multiply solver work when SymPy already has a closed form.
|
|
748
|
+
try:
|
|
749
|
+
global_sol = sp.solveset(eq, var, domain=part.domain)
|
|
750
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
751
|
+
global_sol = sp.ConditionSet(var, eq, part.domain)
|
|
752
|
+
if not isinstance(global_sol, sp.ConditionSet) and not getattr(
|
|
753
|
+
global_sol, "has", lambda *_: False
|
|
754
|
+
)(sp.ConditionSet):
|
|
755
|
+
return InverseSolutionCertificate(
|
|
756
|
+
eq,
|
|
757
|
+
part.domain,
|
|
758
|
+
global_sol,
|
|
759
|
+
True,
|
|
760
|
+
part,
|
|
761
|
+
evidence=(part,),
|
|
762
|
+
source="global-solve+monotonic-partition",
|
|
763
|
+
)
|
|
764
|
+
sols = []
|
|
765
|
+
isolated = []
|
|
766
|
+
complete = True
|
|
767
|
+
for cell in part.cells:
|
|
768
|
+
if not isinstance(cell.interval, sp.Interval):
|
|
769
|
+
complete = False
|
|
770
|
+
continue
|
|
771
|
+
image = _cell_image(cell)
|
|
772
|
+
if image is not None:
|
|
773
|
+
try:
|
|
774
|
+
contained = image.contains(value)
|
|
775
|
+
if contained is S.false or contained is False:
|
|
776
|
+
continue
|
|
777
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
778
|
+
pass
|
|
779
|
+
try:
|
|
780
|
+
s = sp.solveset(eq, var, domain=cell.interval)
|
|
781
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
782
|
+
s = sp.ConditionSet(var, eq, cell.interval)
|
|
783
|
+
if isinstance(s, sp.ConditionSet) or getattr(s, "has", lambda *_: False)(
|
|
784
|
+
sp.ConditionSet
|
|
785
|
+
):
|
|
786
|
+
certified = _certified_inverse_cell(expr, var, value, cell)
|
|
787
|
+
if certified is None:
|
|
788
|
+
complete = False
|
|
789
|
+
sols.append(s)
|
|
790
|
+
else:
|
|
791
|
+
exact_part, isolated_part, cell_complete = certified
|
|
792
|
+
sols.append(exact_part)
|
|
793
|
+
isolated.extend(isolated_part)
|
|
794
|
+
complete = complete and cell_complete
|
|
795
|
+
else:
|
|
796
|
+
sols.append(s)
|
|
797
|
+
# Include attained shared/end boundaries exactly once; solveset on open
|
|
798
|
+
# cells intentionally excludes them.
|
|
799
|
+
boundaries = []
|
|
800
|
+
if isinstance(part.domain, sp.Interval):
|
|
801
|
+
if not part.domain.left_open:
|
|
802
|
+
boundaries.append(part.domain.start)
|
|
803
|
+
if not part.domain.right_open:
|
|
804
|
+
boundaries.append(part.domain.end)
|
|
805
|
+
boundaries.extend(part.critical_points)
|
|
806
|
+
for q in boundaries:
|
|
807
|
+
try:
|
|
808
|
+
if sp.simplify(expr.subs(var, q) - value).is_zero is True:
|
|
809
|
+
sols.append(sp.FiniteSet(q))
|
|
810
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
811
|
+
pass
|
|
812
|
+
sol = sp.Union(*sols) if sols else S.EmptySet
|
|
813
|
+
return InverseSolutionCertificate(
|
|
814
|
+
eq,
|
|
815
|
+
part.domain,
|
|
816
|
+
sol,
|
|
817
|
+
complete,
|
|
818
|
+
part,
|
|
819
|
+
evidence=(part,),
|
|
820
|
+
isolated_intervals=tuple(isolated),
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def certified_monotonic_inverse(expr, var, value, *, domain=True, partition=None):
|
|
825
|
+
"""Certified reusable inverse primitive for a monotonic real function.
|
|
826
|
+
|
|
827
|
+
This is the explicit certification-oriented spelling of
|
|
828
|
+
:func:`solve_monotonic_inverse`; the latter is retained for compatibility.
|
|
829
|
+
"""
|
|
830
|
+
return solve_monotonic_inverse(expr, var, value, domain=domain, partition=partition)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def quick_nonmonotonicity_witness(expr, var, domain):
|
|
834
|
+
"""Try exact rational points and return only an exactly verified violation."""
|
|
835
|
+
ds = domain if isinstance(domain, sp.Set) else condition_to_set(domain, var)
|
|
836
|
+
if (
|
|
837
|
+
not isinstance(ds, sp.Interval)
|
|
838
|
+
or ds.start in (S.NegativeInfinity, S.Infinity)
|
|
839
|
+
or ds.end in (S.NegativeInfinity, S.Infinity)
|
|
840
|
+
):
|
|
841
|
+
return None
|
|
842
|
+
a, b = ds.start, ds.end
|
|
843
|
+
pts = tuple(
|
|
844
|
+
sp.simplify(a + (b - a) * q)
|
|
845
|
+
for q in (
|
|
846
|
+
sp.Rational(1, 5),
|
|
847
|
+
sp.Rational(2, 5),
|
|
848
|
+
sp.Rational(3, 5),
|
|
849
|
+
sp.Rational(4, 5),
|
|
850
|
+
)
|
|
851
|
+
)
|
|
852
|
+
vals = tuple(sp.simplify(expr.subs(var, p)) for p in pts)
|
|
853
|
+
signs = []
|
|
854
|
+
for u, v in itertools.pairwise(vals):
|
|
855
|
+
d = sp.simplify(v - u)
|
|
856
|
+
if d.is_positive is True:
|
|
857
|
+
signs.append(1)
|
|
858
|
+
elif d.is_negative is True:
|
|
859
|
+
signs.append(-1)
|
|
860
|
+
elif d.is_zero is True:
|
|
861
|
+
signs.append(0)
|
|
862
|
+
else:
|
|
863
|
+
return None
|
|
864
|
+
nz = {s for s in signs if s}
|
|
865
|
+
if len(nz) > 1:
|
|
866
|
+
return NonMonotonicityWitness(pts, vals, True)
|
|
867
|
+
return None
|