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,222 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from math import gcd, lcm
|
|
5
|
+
|
|
6
|
+
import sympy as sp
|
|
7
|
+
from sympy import S
|
|
8
|
+
|
|
9
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _canonical_period(period):
|
|
13
|
+
p = sp.simplify(sp.sympify(period))
|
|
14
|
+
if p.is_negative is True:
|
|
15
|
+
p = -p
|
|
16
|
+
return p
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _integer_ratio(a, b):
|
|
20
|
+
r = sp.simplify(a / b)
|
|
21
|
+
return int(r) if r.is_Integer is True and int(r) > 0 else None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _translate_set(s, delta):
|
|
25
|
+
delta = sp.sympify(delta)
|
|
26
|
+
if delta == 0:
|
|
27
|
+
return s
|
|
28
|
+
if isinstance(s, sp.Interval):
|
|
29
|
+
return sp.Interval(s.start + delta, s.end + delta, s.left_open, s.right_open)
|
|
30
|
+
if isinstance(s, sp.FiniteSet):
|
|
31
|
+
return sp.FiniteSet(*(sp.simplify(v + delta) for v in s))
|
|
32
|
+
if isinstance(s, sp.Union):
|
|
33
|
+
return sp.Union(*(_translate_set(a, delta) for a in s.args))
|
|
34
|
+
return sp.imageset(lambda t: t + delta, s)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class PeriodicSet:
|
|
39
|
+
"""Set obtained by translating a base set by integer multiples of a period."""
|
|
40
|
+
|
|
41
|
+
base: sp.Set
|
|
42
|
+
period: sp.Expr
|
|
43
|
+
variable: sp.Symbol | None = None
|
|
44
|
+
origin: sp.Expr = S.Zero
|
|
45
|
+
|
|
46
|
+
def __post_init__(self):
|
|
47
|
+
object.__setattr__(self, "base", sp.sympify(self.base))
|
|
48
|
+
object.__setattr__(self, "period", _canonical_period(self.period))
|
|
49
|
+
object.__setattr__(self, "origin", sp.sympify(self.origin))
|
|
50
|
+
if self.period.is_positive is not True:
|
|
51
|
+
raise ValueError("PeriodicSet period must be provably positive")
|
|
52
|
+
object.__setattr__(
|
|
53
|
+
self, "base", sp.Intersection(self.base, self.fundamental_interval)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def fundamental_interval(self):
|
|
58
|
+
return sp.Interval.Ropen(self.origin, self.origin + self.period)
|
|
59
|
+
|
|
60
|
+
def normalize_point(self, value):
|
|
61
|
+
return sp.simplify(
|
|
62
|
+
self.origin + sp.Mod(sp.sympify(value) - self.origin, self.period)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def contains(self, value):
|
|
66
|
+
return sp.Contains(self.normalize_point(value), self.base)
|
|
67
|
+
|
|
68
|
+
def as_condition(self, variable=None):
|
|
69
|
+
v = sp.sympify(variable or self.variable)
|
|
70
|
+
if v is None:
|
|
71
|
+
raise ValueError("a variable is required")
|
|
72
|
+
return self.contains(v)
|
|
73
|
+
|
|
74
|
+
def intersect_fundamental(self, condition_or_set, variable=None):
|
|
75
|
+
if isinstance(condition_or_set, sp.Set):
|
|
76
|
+
s = condition_or_set
|
|
77
|
+
else:
|
|
78
|
+
sp.sympify(variable or self.variable)
|
|
79
|
+
try:
|
|
80
|
+
s = sp.sympify(condition_or_set).as_set()
|
|
81
|
+
except (AttributeError, *SYMBOLIC_QUERY_ERRORS):
|
|
82
|
+
return None
|
|
83
|
+
return sp.Intersection(s, self.fundamental_interval)
|
|
84
|
+
|
|
85
|
+
def with_period(self, new_period):
|
|
86
|
+
"""Represent the same periodic set on a larger commensurate period."""
|
|
87
|
+
new_period = _canonical_period(new_period)
|
|
88
|
+
n = _integer_ratio(new_period, self.period)
|
|
89
|
+
if n is None:
|
|
90
|
+
raise ValueError("new period must be a positive integer multiple")
|
|
91
|
+
pieces = [_translate_set(self.base, k * self.period) for k in range(n)]
|
|
92
|
+
return PeriodicSet(sp.Union(*pieces), new_period, self.variable, self.origin)
|
|
93
|
+
|
|
94
|
+
def _align(self, other):
|
|
95
|
+
if not isinstance(other, PeriodicSet):
|
|
96
|
+
return None
|
|
97
|
+
if sp.simplify(self.origin - other.origin) != 0:
|
|
98
|
+
return None
|
|
99
|
+
p = common_period_values(self.period, other.period)
|
|
100
|
+
if p is None:
|
|
101
|
+
return None
|
|
102
|
+
return self.with_period(p), other.with_period(p)
|
|
103
|
+
|
|
104
|
+
def union(self, other):
|
|
105
|
+
aligned = self._align(other)
|
|
106
|
+
if aligned is None:
|
|
107
|
+
return sp.Union(self.as_sympy_set(), other.as_sympy_set())
|
|
108
|
+
a, b = aligned
|
|
109
|
+
return PeriodicSet(
|
|
110
|
+
sp.Union(a.base, b.base),
|
|
111
|
+
a.period,
|
|
112
|
+
self.variable or other.variable,
|
|
113
|
+
a.origin,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def intersection(self, other):
|
|
117
|
+
aligned = self._align(other)
|
|
118
|
+
if aligned is None:
|
|
119
|
+
return sp.Intersection(self.as_sympy_set(), other.as_sympy_set())
|
|
120
|
+
a, b = aligned
|
|
121
|
+
return PeriodicSet(
|
|
122
|
+
sp.Intersection(a.base, b.base),
|
|
123
|
+
a.period,
|
|
124
|
+
self.variable or other.variable,
|
|
125
|
+
a.origin,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def complement(self):
|
|
129
|
+
return PeriodicSet(
|
|
130
|
+
sp.Complement(self.fundamental_interval, self.base),
|
|
131
|
+
self.period,
|
|
132
|
+
self.variable,
|
|
133
|
+
self.origin,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def affine_pullback(self, a, b=0, variable=None):
|
|
137
|
+
"""Exact pullback under ``t = a*x + b`` for nonzero real ``a``.
|
|
138
|
+
|
|
139
|
+
The previous implementation normalized by inspecting only five nearby
|
|
140
|
+
period copies, which was not valid for arbitrarily large/symbolic
|
|
141
|
+
translations. A modular ConditionSet is exact for every shift.
|
|
142
|
+
"""
|
|
143
|
+
a = sp.sympify(a)
|
|
144
|
+
b = sp.sympify(b)
|
|
145
|
+
if a == 0 or a.is_real is False:
|
|
146
|
+
raise ValueError("a must be nonzero real")
|
|
147
|
+
if a.is_positive is not True and a.is_negative is not True:
|
|
148
|
+
raise ValueError("sign of a must be known")
|
|
149
|
+
p = sp.simplify(self.period / sp.Abs(a))
|
|
150
|
+
x = sp.sympify(variable or self.variable or sp.Dummy("x", real=True))
|
|
151
|
+
fi = sp.Interval.Ropen(S.Zero, p)
|
|
152
|
+
# self.contains performs exact modular normalization in the source
|
|
153
|
+
# period, so no finite translation window is needed.
|
|
154
|
+
cond = self.contains(sp.simplify(a * x + b))
|
|
155
|
+
base = sp.ConditionSet(x, cond, fi)
|
|
156
|
+
# Simplify common finite-set cases without weakening the exact fallback.
|
|
157
|
+
if isinstance(self.base, sp.FiniteSet):
|
|
158
|
+
pts = []
|
|
159
|
+
for q in self.base:
|
|
160
|
+
pts.append(sp.simplify(sp.Mod((q - b) / a, p)))
|
|
161
|
+
base = sp.FiniteSet(*pts)
|
|
162
|
+
return PeriodicSet(base, p, x, S.Zero)
|
|
163
|
+
|
|
164
|
+
def as_sympy_set(self):
|
|
165
|
+
sp.Dummy("k", integer=True)
|
|
166
|
+
return sp.imageset(lambda t, k: t + k * self.period, self.base, S.Integers)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(frozen=True)
|
|
170
|
+
class PeriodicCondition:
|
|
171
|
+
set: PeriodicSet
|
|
172
|
+
|
|
173
|
+
def as_expr(self, variable=None):
|
|
174
|
+
return self.set.as_condition(variable)
|
|
175
|
+
|
|
176
|
+
def __sympy__(self):
|
|
177
|
+
return self.as_expr()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def positive_period(expr, var):
|
|
181
|
+
try:
|
|
182
|
+
from sympy.calculus.util import periodicity
|
|
183
|
+
|
|
184
|
+
p = periodicity(sp.sympify(expr), sp.sympify(var))
|
|
185
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
186
|
+
return None
|
|
187
|
+
if p in (None, S.Zero, sp.oo, -sp.oo):
|
|
188
|
+
return None
|
|
189
|
+
p = _canonical_period(p)
|
|
190
|
+
return p if p.is_positive is True else None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def common_period_values(*periods):
|
|
194
|
+
ps = [_canonical_period(p) for p in periods]
|
|
195
|
+
if not ps:
|
|
196
|
+
return None
|
|
197
|
+
p0 = ps[0]
|
|
198
|
+
ratios = []
|
|
199
|
+
for p in ps:
|
|
200
|
+
r = sp.simplify(p / p0)
|
|
201
|
+
if r.is_Rational is not True:
|
|
202
|
+
return None
|
|
203
|
+
ratios.append(sp.Rational(r))
|
|
204
|
+
nums = [abs(int(r.p)) for r in ratios]
|
|
205
|
+
dens = [int(r.q) for r in ratios]
|
|
206
|
+
num = 1
|
|
207
|
+
for n in nums:
|
|
208
|
+
num = lcm(num, n)
|
|
209
|
+
den = dens[0]
|
|
210
|
+
for d in dens[1:]:
|
|
211
|
+
den = gcd(den, d)
|
|
212
|
+
return sp.simplify(p0 * sp.Rational(num, den))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def common_positive_period(expressions, var):
|
|
216
|
+
ps = []
|
|
217
|
+
for e in expressions:
|
|
218
|
+
p = positive_period(e, var)
|
|
219
|
+
if p is None:
|
|
220
|
+
return None
|
|
221
|
+
ps.append(p)
|
|
222
|
+
return common_period_values(*ps)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
import sympy as sp
|
|
7
|
+
from sympy import S
|
|
8
|
+
|
|
9
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PerformanceGoal(str, Enum):
|
|
13
|
+
SPEED = "speed"
|
|
14
|
+
QUALITY = "quality"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ProofPolicy:
|
|
19
|
+
goal: PerformanceGoal = PerformanceGoal.QUALITY
|
|
20
|
+
allow_numerical_evidence: bool = True
|
|
21
|
+
require_certificate: bool = True
|
|
22
|
+
max_symbolic_recursion: int = 3
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extended_real_sign(value):
|
|
26
|
+
value = sp.sympify(value)
|
|
27
|
+
if value is sp.oo:
|
|
28
|
+
return 1
|
|
29
|
+
if value is -sp.oo:
|
|
30
|
+
return -1
|
|
31
|
+
if value == 0 or value.is_zero is True:
|
|
32
|
+
return 0
|
|
33
|
+
if value.is_positive is True:
|
|
34
|
+
return 1
|
|
35
|
+
if value.is_negative is True:
|
|
36
|
+
return -1
|
|
37
|
+
# Do not certify a sign from a fixed-precision approximation. This helper
|
|
38
|
+
# is used by proof infrastructure, so expressions too close to zero (or
|
|
39
|
+
# whose exact comparison is simply unknown) must remain unknown.
|
|
40
|
+
try:
|
|
41
|
+
q = sp.ask(sp.Q.positive(value))
|
|
42
|
+
if q is True:
|
|
43
|
+
return 1
|
|
44
|
+
q = sp.ask(sp.Q.negative(value))
|
|
45
|
+
if q is True:
|
|
46
|
+
return -1
|
|
47
|
+
q = sp.ask(sp.Q.zero(value))
|
|
48
|
+
if q is True:
|
|
49
|
+
return 0
|
|
50
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
51
|
+
pass
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def find_parameters(expressions, variables):
|
|
56
|
+
"""Return free symbolic parameters independent of ``variables``."""
|
|
57
|
+
variables = set(map(sp.sympify, variables))
|
|
58
|
+
if not isinstance(expressions, (tuple, list, set, frozenset)):
|
|
59
|
+
expressions = (expressions,)
|
|
60
|
+
free = set()
|
|
61
|
+
for e in expressions:
|
|
62
|
+
if isinstance(e, sp.Basic):
|
|
63
|
+
free.update(e.free_symbols)
|
|
64
|
+
return tuple(sorted(free - variables, key=sp.default_sort_key))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def condition_to_intervals(condition, var, *, integers=False):
|
|
68
|
+
"""Parse a solved univariate condition into ordered intervals/points.
|
|
69
|
+
|
|
70
|
+
This is the SymPy analogue of the useful CADToIntervals helper in the
|
|
71
|
+
supplied FunctionProperties source. ``None`` means the condition is not
|
|
72
|
+
in a supported solved form.
|
|
73
|
+
"""
|
|
74
|
+
condition = sp.sympify(condition)
|
|
75
|
+
var = sp.sympify(var)
|
|
76
|
+
try:
|
|
77
|
+
s = (
|
|
78
|
+
condition.as_set()
|
|
79
|
+
if condition not in (S.true, S.false)
|
|
80
|
+
else (S.Reals if condition is S.true else S.EmptySet)
|
|
81
|
+
)
|
|
82
|
+
except (AttributeError, *SYMBOLIC_QUERY_ERRORS):
|
|
83
|
+
return None
|
|
84
|
+
if integers:
|
|
85
|
+
s = sp.Intersection(s, S.Integers)
|
|
86
|
+
out = []
|
|
87
|
+
|
|
88
|
+
def visit(t):
|
|
89
|
+
if t is S.EmptySet:
|
|
90
|
+
return True
|
|
91
|
+
if isinstance(t, sp.Interval):
|
|
92
|
+
out.append(t)
|
|
93
|
+
return True
|
|
94
|
+
if isinstance(t, sp.FiniteSet):
|
|
95
|
+
out.extend(sp.FiniteSet(x) for x in sorted(t, key=sp.default_sort_key))
|
|
96
|
+
return True
|
|
97
|
+
if isinstance(t, sp.Union):
|
|
98
|
+
return all(visit(a) for a in t.args)
|
|
99
|
+
if isinstance(t, sp.Range):
|
|
100
|
+
out.append(t)
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
return tuple(out) if visit(s) else None
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import mpmath as mp
|
|
6
|
+
import sympy as sp
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class RealIntervalEvaluation:
|
|
11
|
+
expression: sp.Basic
|
|
12
|
+
variable: sp.Symbol
|
|
13
|
+
domain: sp.Interval
|
|
14
|
+
lower: str
|
|
15
|
+
upper: str
|
|
16
|
+
excludes_zero: bool
|
|
17
|
+
strict_sign: int
|
|
18
|
+
source: str = "mpmath-outward-rounded-real-interval"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _const_iv(expr):
|
|
22
|
+
if isinstance(expr, sp.Integer):
|
|
23
|
+
n = int(expr)
|
|
24
|
+
return mp.iv.mpf([n, n])
|
|
25
|
+
if isinstance(expr, sp.Rational):
|
|
26
|
+
return mp.iv.mpf([int(expr.p), int(expr.p)]) / mp.iv.mpf(
|
|
27
|
+
[int(expr.q), int(expr.q)]
|
|
28
|
+
)
|
|
29
|
+
if expr == sp.pi:
|
|
30
|
+
return mp.iv.pi
|
|
31
|
+
if expr == sp.E:
|
|
32
|
+
return mp.iv.e
|
|
33
|
+
raise ValueError
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _eval(expr, var, x):
|
|
37
|
+
expr = sp.sympify(expr)
|
|
38
|
+
if expr == var:
|
|
39
|
+
return x
|
|
40
|
+
if not expr.has(var) and (
|
|
41
|
+
isinstance(expr, (sp.Integer, sp.Rational)) or expr in (sp.pi, sp.E)
|
|
42
|
+
):
|
|
43
|
+
return _const_iv(expr)
|
|
44
|
+
if expr.is_Add:
|
|
45
|
+
out = mp.iv.mpf([0, 0])
|
|
46
|
+
for arg in expr.args:
|
|
47
|
+
out += _eval(arg, var, x)
|
|
48
|
+
return out
|
|
49
|
+
if expr.is_Mul:
|
|
50
|
+
out = mp.iv.mpf([1, 1])
|
|
51
|
+
for arg in expr.args:
|
|
52
|
+
out *= _eval(arg, var, x)
|
|
53
|
+
return out
|
|
54
|
+
if expr.is_Pow:
|
|
55
|
+
base, exponent = expr.args
|
|
56
|
+
b = _eval(base, var, x)
|
|
57
|
+
if exponent.is_Integer:
|
|
58
|
+
return b ** int(exponent)
|
|
59
|
+
if exponent.is_Rational and float(b.a) > 0:
|
|
60
|
+
q = _const_iv(exponent)
|
|
61
|
+
return mp.iv.exp(q * mp.iv.log(b))
|
|
62
|
+
raise ValueError
|
|
63
|
+
if expr.func is sp.exp:
|
|
64
|
+
return mp.iv.exp(_eval(expr.args[0], var, x))
|
|
65
|
+
if expr.func is sp.log:
|
|
66
|
+
a = _eval(expr.args[0], var, x)
|
|
67
|
+
if float(a.a) <= 0:
|
|
68
|
+
raise ValueError
|
|
69
|
+
return mp.iv.log(a)
|
|
70
|
+
if expr.func is sp.sin:
|
|
71
|
+
return mp.iv.sin(_eval(expr.args[0], var, x))
|
|
72
|
+
if expr.func is sp.cos:
|
|
73
|
+
return mp.iv.cos(_eval(expr.args[0], var, x))
|
|
74
|
+
if expr.func is sp.tan:
|
|
75
|
+
return mp.iv.tan(_eval(expr.args[0], var, x))
|
|
76
|
+
if expr.func is sp.sinh:
|
|
77
|
+
a = _eval(expr.args[0], var, x)
|
|
78
|
+
return (mp.iv.exp(a) - mp.iv.exp(-a)) / 2
|
|
79
|
+
if expr.func is sp.cosh:
|
|
80
|
+
a = _eval(expr.args[0], var, x)
|
|
81
|
+
return (mp.iv.exp(a) + mp.iv.exp(-a)) / 2
|
|
82
|
+
if expr.func is sp.tanh:
|
|
83
|
+
a = _eval(expr.args[0], var, x)
|
|
84
|
+
e = mp.iv.exp(2 * a)
|
|
85
|
+
return (e - 1) / (e + 1)
|
|
86
|
+
raise ValueError
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def certified_real_interval_evaluate(expr, var, domain):
|
|
90
|
+
"""Enclose ``expr(domain)`` for a finite exact real interval.
|
|
91
|
+
|
|
92
|
+
The routine intentionally supports a bounded elementary vocabulary and returns
|
|
93
|
+
``None`` outside it. Its purpose is a conservative root-isolation fallback:
|
|
94
|
+
excluding zero proves a cell root-free, and excluding zero from a derivative
|
|
95
|
+
enclosure proves strict monotonicity on that cell.
|
|
96
|
+
"""
|
|
97
|
+
expr = sp.sympify(expr)
|
|
98
|
+
var = sp.sympify(var)
|
|
99
|
+
if (
|
|
100
|
+
not isinstance(domain, sp.Interval)
|
|
101
|
+
or domain.start in (-sp.oo, sp.oo)
|
|
102
|
+
or domain.end in (-sp.oo, sp.oo)
|
|
103
|
+
or domain.start.is_real is False
|
|
104
|
+
or domain.end.is_real is False
|
|
105
|
+
):
|
|
106
|
+
return None
|
|
107
|
+
try:
|
|
108
|
+
a = _const_iv(domain.start)
|
|
109
|
+
b = _const_iv(domain.end)
|
|
110
|
+
x = mp.iv.mpf([a.a, b.b])
|
|
111
|
+
value = _eval(expr, var, x)
|
|
112
|
+
lo = float(value.a)
|
|
113
|
+
hi = float(value.b)
|
|
114
|
+
except (ValueError, TypeError, OverflowError, ZeroDivisionError):
|
|
115
|
+
return None
|
|
116
|
+
sign = 1 if lo > 0 else -1 if hi < 0 else 0
|
|
117
|
+
return RealIntervalEvaluation(
|
|
118
|
+
expr,
|
|
119
|
+
var,
|
|
120
|
+
domain,
|
|
121
|
+
str(value.a),
|
|
122
|
+
str(value.b),
|
|
123
|
+
sign != 0,
|
|
124
|
+
sign,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
__all__ = ["RealIntervalEvaluation", "certified_real_interval_evaluate"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
|
|
5
|
+
import mpmath as mp
|
|
6
|
+
import sympy as sp
|
|
7
|
+
|
|
8
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _exact_sign(expr):
|
|
12
|
+
"""Return -1, 0, or 1 when SymPy establishes the real sign."""
|
|
13
|
+
expr = sp.sympify(expr)
|
|
14
|
+
with contextlib.suppress(*SYMBOLIC_QUERY_ERRORS):
|
|
15
|
+
expr = sp.simplify(expr)
|
|
16
|
+
if expr.is_zero is True:
|
|
17
|
+
return 0
|
|
18
|
+
if expr.is_positive is True:
|
|
19
|
+
return 1
|
|
20
|
+
if expr.is_negative is True:
|
|
21
|
+
return -1
|
|
22
|
+
with contextlib.suppress(*SYMBOLIC_QUERY_ERRORS):
|
|
23
|
+
q = sp.sign(expr)
|
|
24
|
+
if q in (-1, 0, 1):
|
|
25
|
+
return int(q)
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _iv_rational(q):
|
|
30
|
+
if isinstance(q, sp.Integer):
|
|
31
|
+
return mp.iv.mpf([int(q), int(q)])
|
|
32
|
+
if isinstance(q, sp.Rational):
|
|
33
|
+
return mp.iv.mpf([int(q.p), int(q.p)]) / mp.iv.mpf([int(q.q), int(q.q)])
|
|
34
|
+
raise ValueError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _interval_eval(expr):
|
|
38
|
+
"""Evaluate a numeric exp/log elementary expression with interval arithmetic.
|
|
39
|
+
|
|
40
|
+
This intentionally supports a small audited vocabulary. Unsupported heads raise
|
|
41
|
+
``ValueError`` so callers remain conservative rather than silently falling back
|
|
42
|
+
to ordinary floating-point evaluation.
|
|
43
|
+
"""
|
|
44
|
+
expr = sp.sympify(expr)
|
|
45
|
+
if isinstance(expr, (sp.Integer, sp.Rational)):
|
|
46
|
+
return _iv_rational(expr)
|
|
47
|
+
if expr == sp.pi:
|
|
48
|
+
return mp.iv.pi
|
|
49
|
+
if expr == sp.E:
|
|
50
|
+
return mp.iv.e
|
|
51
|
+
if expr.is_Add:
|
|
52
|
+
out = mp.iv.mpf([0, 0])
|
|
53
|
+
for arg in expr.args:
|
|
54
|
+
out += _interval_eval(arg)
|
|
55
|
+
return out
|
|
56
|
+
if expr.is_Mul:
|
|
57
|
+
out = mp.iv.mpf([1, 1])
|
|
58
|
+
for arg in expr.args:
|
|
59
|
+
out *= _interval_eval(arg)
|
|
60
|
+
return out
|
|
61
|
+
if expr.is_Pow:
|
|
62
|
+
base, exponent = expr.args
|
|
63
|
+
b = _interval_eval(base)
|
|
64
|
+
if exponent.is_Integer:
|
|
65
|
+
return b ** int(exponent)
|
|
66
|
+
if exponent.is_Rational:
|
|
67
|
+
# Only use real positive-base rational powers in the robust sign path.
|
|
68
|
+
if float(b.a) <= 0:
|
|
69
|
+
raise ValueError
|
|
70
|
+
return mp.iv.exp(_iv_rational(exponent) * mp.iv.log(b))
|
|
71
|
+
raise ValueError
|
|
72
|
+
if expr.func is sp.exp:
|
|
73
|
+
return mp.iv.exp(_interval_eval(expr.args[0]))
|
|
74
|
+
if expr.func is sp.log:
|
|
75
|
+
x = _interval_eval(expr.args[0])
|
|
76
|
+
if float(x.a) <= 0:
|
|
77
|
+
raise ValueError
|
|
78
|
+
return mp.iv.log(x)
|
|
79
|
+
if expr.func is sp.sinh:
|
|
80
|
+
x = _interval_eval(expr.args[0])
|
|
81
|
+
return (mp.iv.exp(x) - mp.iv.exp(-x)) / 2
|
|
82
|
+
if expr.func is sp.cosh:
|
|
83
|
+
x = _interval_eval(expr.args[0])
|
|
84
|
+
return (mp.iv.exp(x) + mp.iv.exp(-x)) / 2
|
|
85
|
+
if expr.func is sp.tanh:
|
|
86
|
+
x = _interval_eval(expr.args[0])
|
|
87
|
+
ex = mp.iv.exp(2 * x)
|
|
88
|
+
return (ex - 1) / (ex + 1)
|
|
89
|
+
raise ValueError
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def robust_real_sign(expr, *, interval_fallback=True):
|
|
93
|
+
"""Return a certified sign for a real numeric expression when possible.
|
|
94
|
+
|
|
95
|
+
The exact symbolic route is tried first. For unresolved numeric expressions in
|
|
96
|
+
an audited exp/log vocabulary, interval arithmetic is used so very large or very
|
|
97
|
+
small exponentials can be compared without overflow/underflow. A numerical
|
|
98
|
+
interval containing zero is *not* converted into a sign.
|
|
99
|
+
"""
|
|
100
|
+
expr = sp.sympify(expr)
|
|
101
|
+
sign = _exact_sign(expr)
|
|
102
|
+
if sign is not None or not interval_fallback:
|
|
103
|
+
return sign
|
|
104
|
+
if expr.free_symbols or expr.is_real is False:
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
value = _interval_eval(expr)
|
|
108
|
+
if float(value.a) > 0:
|
|
109
|
+
return 1
|
|
110
|
+
if float(value.b) < 0:
|
|
111
|
+
return -1
|
|
112
|
+
except (ValueError, TypeError, OverflowError, ZeroDivisionError):
|
|
113
|
+
return None
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = ["robust_real_sign"]
|