funcprops 0.1.0__py3-none-any.whl

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