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,561 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+
5
+ import sympy as sp
6
+ from sympy import S
7
+
8
+ from .. import _semialg
9
+ from ..engine import evaluate_property
10
+ from ..exceptions import SYMBOLIC_QUERY_ERRORS
11
+ from ..results import ConditionalValue, PropertyResult, Sign, TruthValue
12
+ from ..stratification import (
13
+ evaluate_parameter_strata,
14
+ implication_guard,
15
+ normalize_cases,
16
+ semantic_condition,
17
+ )
18
+ from ..transcendental.algebraize import algebraize
19
+ from ..transcendental.roots import transcendental_roots
20
+ from .common import (
21
+ combine_natural_domain_condition,
22
+ condition_to_set,
23
+ exact_order_real_points,
24
+ )
25
+ from .domain import function_domain
26
+ from .piecewise import combine_piecewise_sign, piecewise_branches
27
+ from .special_functions import positive_axis_sign_strata
28
+
29
+
30
+ def _constant_sign(expr):
31
+ if expr == 0:
32
+ return Sign.ZERO
33
+ if expr.is_positive is True:
34
+ return Sign.POSITIVE
35
+ if expr.is_negative is True:
36
+ return Sign.NEGATIVE
37
+ if expr.is_nonnegative is True:
38
+ return Sign.NONNEGATIVE
39
+ if expr.is_nonpositive is True:
40
+ return Sign.NONPOSITIVE
41
+ return Sign.UNKNOWN
42
+
43
+
44
+ def _registry_sign(expr, var, assumptions=S.true):
45
+ """Use catalogue sign loci as global sign proofs/parameter strata.
46
+
47
+ Registry ``Positive`` etc. values are predicates in the function variable,
48
+ not booleans saying that the function is globally positive. We therefore
49
+ prove that the requested domain implies each locus and retain any residual
50
+ parameter-only guard.
51
+ """
52
+ cases = []
53
+ for prop, val in (
54
+ ("Positive", Sign.POSITIVE),
55
+ ("Negative", Sign.NEGATIVE),
56
+ ("NonNegative", Sign.NONNEGATIVE),
57
+ ("NonPositive", Sign.NONPOSITIVE),
58
+ ):
59
+ r = evaluate_property(expr, prop, var=var, mode="real", assumptions=assumptions)
60
+ rcases = r.cases or (
61
+ () if r.primary is None else (ConditionalValue(r.primary, S.true),)
62
+ )
63
+ for c in rcases:
64
+ guard = implication_guard(assumptions, c.value, var)
65
+ if guard is None:
66
+ continue
67
+ try:
68
+ guard = sp.simplify_logic(
69
+ sp.And(semantic_condition(c.condition), guard)
70
+ )
71
+ except SYMBOLIC_QUERY_ERRORS:
72
+ guard = sp.And(semantic_condition(c.condition), guard)
73
+ if guard is not S.false:
74
+ cases.append(ConditionalValue(val, guard))
75
+ if not cases:
76
+ return Sign.UNKNOWN
77
+ out = normalize_cases(cases, source="registry-sign-stratification")
78
+ return out.primary if out.primary is not None else out
79
+
80
+
81
+ def _constant_sign_strata(expr):
82
+ """Exact parameter strata for a real constant with unresolved sign."""
83
+ expr = sp.sympify(expr)
84
+ if not expr.free_symbols or expr.is_real is False:
85
+ return Sign.UNKNOWN
86
+ return normalize_cases(
87
+ (
88
+ ConditionalValue(Sign.POSITIVE, expr > 0),
89
+ ConditionalValue(Sign.ZERO, sp.Eq(expr, 0)),
90
+ ConditionalValue(Sign.NEGATIVE, expr < 0),
91
+ ),
92
+ source="constant-parameter-sign",
93
+ )
94
+
95
+
96
+ def _semialg_sign(expr, var, assumptions):
97
+ if not _semialg.eligible_problem(expr, var, domain=assumptions):
98
+ return Sign.UNKNOWN
99
+ try:
100
+ raw = _semialg.function_sign(
101
+ expr, var, assumptions=assumptions, return_result=True
102
+ )
103
+ value = _semialg.normalize_sign(raw)
104
+ if value is not Sign.UNKNOWN:
105
+ return value
106
+ pieces = _semialg.function_sign_partition(expr, var, domain=assumptions)
107
+ signs = {
108
+ str(
109
+ getattr(
110
+ piece,
111
+ "classification",
112
+ piece[0] if isinstance(piece, tuple) else piece,
113
+ )
114
+ )
115
+ for piece in pieces
116
+ }
117
+ if "positive" in signs and "negative" in signs:
118
+ return Sign.SIGN_CHANGING
119
+ if signs == {"zero"}:
120
+ return Sign.ZERO
121
+ return Sign.UNKNOWN
122
+ except (_semialg.SemialgUnavailable, *SYMBOLIC_QUERY_ERRORS):
123
+ return Sign.UNKNOWN
124
+
125
+
126
+ def _multiply_sign_values(a, b):
127
+ if a is Sign.ZERO or b is Sign.ZERO:
128
+ return Sign.ZERO
129
+ strict = {Sign.POSITIVE: 1, Sign.NEGATIVE: -1}
130
+ weak = {Sign.NONNEGATIVE: 1, Sign.NONPOSITIVE: -1}
131
+ if a in strict and b in strict:
132
+ return Sign.POSITIVE if strict[a] * strict[b] > 0 else Sign.NEGATIVE
133
+ if a in strict or a in weak:
134
+ sa = strict.get(a, weak.get(a))
135
+ if b in strict or b in weak:
136
+ sb = strict.get(b, weak.get(b))
137
+ return Sign.NONNEGATIVE if sa * sb > 0 else Sign.NONPOSITIVE
138
+ return Sign.UNKNOWN
139
+
140
+
141
+ def _multiply_sign_results(values):
142
+ cases = [ConditionalValue(Sign.POSITIVE, S.true)]
143
+ conditional = False
144
+ for value in values:
145
+ vcases = (
146
+ value.cases
147
+ if isinstance(value, PropertyResult)
148
+ else (ConditionalValue(value, S.true),)
149
+ )
150
+ conditional = conditional or isinstance(value, PropertyResult)
151
+ new = []
152
+ for a in cases:
153
+ for b in vcases:
154
+ v = _multiply_sign_values(a.value, b.value)
155
+ if v is Sign.UNKNOWN:
156
+ continue
157
+ try:
158
+ cond = sp.simplify_logic(sp.And(a.condition, b.condition))
159
+ except SYMBOLIC_QUERY_ERRORS:
160
+ cond = sp.And(a.condition, b.condition)
161
+ if cond is not S.false:
162
+ new.append(ConditionalValue(v, cond))
163
+ if not new:
164
+ return Sign.UNKNOWN
165
+ cases = new
166
+ out = normalize_cases(cases, source="product-sign-stratification")
167
+ if not conditional and out.primary is not None:
168
+ return out.primary
169
+ return out.primary if out.primary is not None else out
170
+
171
+
172
+ def _is_positive_axis_domain(domain, var):
173
+ ds = condition_to_set(domain, var)
174
+ if (
175
+ isinstance(ds, sp.Interval)
176
+ and ds.start == 0
177
+ and ds.left_open
178
+ and ds.end is S.Infinity
179
+ ):
180
+ return True
181
+ try:
182
+ return (
183
+ sp.simplify_logic(sp.Implies(semantic_condition(domain), var > 0)) is S.true
184
+ )
185
+ except SYMBOLIC_QUERY_ERRORS:
186
+ return False
187
+
188
+
189
+ def _special_sign_strata(expr, var, domain):
190
+ if not _is_positive_axis_domain(domain, var):
191
+ return Sign.UNKNOWN
192
+ facts = positive_axis_sign_strata(expr, var)
193
+ if not facts:
194
+ return Sign.UNKNOWN
195
+ out = normalize_cases(
196
+ (ConditionalValue(v, g) for v, g, *_ in facts),
197
+ source="special-function-sign-stratification",
198
+ )
199
+ return out.primary if out.primary is not None else out
200
+
201
+
202
+ def _structural_sign(expr, var, domain):
203
+ if expr.func is sp.exp:
204
+ return Sign.POSITIVE
205
+ if expr.func is sp.gamma:
206
+ ds = condition_to_set(domain, var)
207
+ pos = (
208
+ isinstance(ds, sp.Interval)
209
+ and ds.start.is_nonnegative is True
210
+ and (ds.start != 0 or ds.left_open)
211
+ )
212
+ pos = (
213
+ pos
214
+ or domain == (var > 0)
215
+ or (isinstance(domain, sp.And) and (var > 0) in domain.args)
216
+ )
217
+ if pos:
218
+ return Sign.POSITIVE
219
+ if (
220
+ expr.func is sp.besselk
221
+ and len(expr.args) == 2
222
+ and expr.args[1] == var
223
+ and expr.args[0].is_real is True
224
+ ):
225
+ ds = condition_to_set(domain, var)
226
+ pos = (
227
+ isinstance(ds, sp.Interval)
228
+ and ds.start.is_nonnegative is True
229
+ and (ds.start != 0 or ds.left_open)
230
+ )
231
+ pos = (
232
+ pos
233
+ or domain == (var > 0)
234
+ or (isinstance(domain, sp.And) and (var > 0) in domain.args)
235
+ )
236
+ if pos:
237
+ return Sign.POSITIVE
238
+ if expr.func is sp.Abs:
239
+ # Strictness follows only if zero can be excluded.
240
+ return Sign.NONNEGATIVE
241
+ if expr.func is sp.Pow:
242
+ base, e = expr.args
243
+ if e.is_Integer and e.is_even:
244
+ if e.is_negative:
245
+ return Sign.POSITIVE
246
+ return Sign.NONNEGATIVE
247
+ if e.is_Integer and e.is_odd:
248
+ return _function_sign(base, var, domain)
249
+ if expr.func is sp.Mul:
250
+ vals = [_function_sign(a, var, domain) for a in expr.args]
251
+ return _multiply_sign_results(vals)
252
+ return Sign.UNKNOWN
253
+
254
+
255
+ def _sample_sign(value):
256
+ value = sp.simplify(value)
257
+ if value.is_positive is True:
258
+ return 1
259
+ if value.is_negative is True:
260
+ return -1
261
+ if value.is_zero is True:
262
+ return 0
263
+ if value.is_number:
264
+ try:
265
+ n = sp.N(value, 30)
266
+ if n.is_real:
267
+ if n > 0:
268
+ return 1
269
+ if n < 0:
270
+ return -1
271
+ if n == 0:
272
+ return 0
273
+ except SYMBOLIC_QUERY_ERRORS:
274
+ pass
275
+ return None
276
+
277
+
278
+ _exact_order_real_points = exact_order_real_points
279
+
280
+
281
+ def _interval_sign(expr, var, domain):
282
+ """Certify sign on a connected interval from continuity and exact zeros."""
283
+ ds = condition_to_set(domain, var)
284
+ if not isinstance(ds, sp.Interval) or ds.measure == 0:
285
+ return Sign.UNKNOWN
286
+ try:
287
+ from sympy.calculus.util import continuous_domain
288
+
289
+ if continuous_domain(expr, var, ds) != ds:
290
+ return Sign.UNKNOWN
291
+ except SYMBOLIC_QUERY_ERRORS:
292
+ return Sign.UNKNOWN
293
+ try:
294
+ zeros = sp.solveset(sp.Eq(expr, 0), var, domain=ds)
295
+ except SYMBOLIC_QUERY_ERRORS:
296
+ return Sign.UNKNOWN
297
+ if zeros is S.EmptySet:
298
+ if ds.start is -sp.oo and ds.end is sp.oo:
299
+ sample = S.Zero
300
+ elif ds.start is -sp.oo:
301
+ sample = ds.end - 1
302
+ elif ds.end is sp.oo:
303
+ sample = ds.start + 1
304
+ else:
305
+ sample = (ds.start + ds.end) / 2
306
+ sg = _sample_sign(expr.subs(var, sample))
307
+ return Sign.POSITIVE if sg == 1 else Sign.NEGATIVE if sg == -1 else Sign.UNKNOWN
308
+ if not isinstance(zeros, sp.FiniteSet):
309
+ return Sign.UNKNOWN
310
+ interior = [z for z in zeros if z in ds and z != ds.start and z != ds.end]
311
+ interior = _exact_order_real_points(interior)
312
+ if interior is None:
313
+ return Sign.UNKNOWN
314
+ pts = [ds.start, *list(interior), ds.end]
315
+ signs = []
316
+ for a, b in itertools.pairwise(pts):
317
+ if a is -sp.oo and b is sp.oo:
318
+ sample = S.Zero
319
+ elif a is -sp.oo:
320
+ sample = b - 1
321
+ elif b is sp.oo:
322
+ sample = a + 1
323
+ else:
324
+ sample = (a + b) / 2
325
+ sg = _sample_sign(expr.subs(var, sample))
326
+ if sg is None:
327
+ return Sign.UNKNOWN
328
+ if sg:
329
+ signs.append(sg)
330
+ if signs and all(s == 1 for s in signs):
331
+ return Sign.NONNEGATIVE
332
+ if signs and all(s == -1 for s in signs):
333
+ return Sign.NONPOSITIVE
334
+ if 1 in signs and -1 in signs:
335
+ return Sign.SIGN_CHANGING
336
+ return Sign.UNKNOWN
337
+
338
+
339
+ def _function_sign(expr, var, domain):
340
+ """Run the shared exact sign-proof ladder on a requested real domain."""
341
+ expr = sp.sympify(expr)
342
+ if isinstance(expr, sp.Piecewise):
343
+ branches = piecewise_branches(expr, var, domain)
344
+ if branches:
345
+ values = [_function_sign(b.expression, var, b.condition) for b in branches]
346
+ combined = combine_piecewise_sign(values)
347
+ if combined is not Sign.UNKNOWN:
348
+ return combined
349
+ if not expr.has(var):
350
+ s = _constant_sign(expr)
351
+ return _constant_sign_strata(expr) if s is Sign.UNKNOWN else s
352
+ # Structural facts are globally valid on the natural domain.
353
+ s = _structural_sign(expr, var, domain)
354
+ if s is not Sign.UNKNOWN:
355
+ return s
356
+ s = _special_sign_strata(expr, var, domain)
357
+ if s is not Sign.UNKNOWN:
358
+ return s
359
+ s = _registry_sign(expr, var, domain)
360
+ if s is not Sign.UNKNOWN:
361
+ return s
362
+ s = _semialg_sign(expr, var, domain)
363
+ if s is not Sign.UNKNOWN:
364
+ return s
365
+
366
+ # Exact transcendental algebraization: e.g. exp(2*x)-3*exp(x)+2 is
367
+ # a polynomial in u=exp(x), u>0. The transformed problem can then use
368
+ # the semialgebraic/interval proof machinery without treating exp as an
369
+ # opaque numeric function.
370
+ ar = algebraize(expr, var)
371
+ if ar.changed and ar.exact and len(ar.substitutions) == 1:
372
+ u, _ = ar.substitutions[0]
373
+ transformed_domain = ar.constraints
374
+ ts = _function_sign(ar.expression, u, transformed_domain)
375
+ if ts is not Sign.UNKNOWN:
376
+ return ts
377
+
378
+ # Certified root partitioning on a single interval. Unlike a numerical
379
+ # scan, this path is used only when the root engine proves completeness.
380
+ ds = condition_to_set(domain, var)
381
+ if isinstance(ds, sp.Interval):
382
+ rr = transcendental_roots(expr, var, ds, allow_numerical=False)
383
+ if rr.complete and rr.periodic is None:
384
+ return _interval_sign(expr, var, domain)
385
+ return _interval_sign(expr, var, domain)
386
+
387
+
388
+ def _function_sign_public_impl(
389
+ expr, var=None, *, domain=True, strict_inequalities=True
390
+ ):
391
+ """Classify the sign of a real expression over its natural/requested domain."""
392
+ expr = sp.sympify(expr)
393
+ if var is None:
394
+ syms = tuple(expr.free_symbols)
395
+ if len(syms) != 1:
396
+ return _constant_sign(expr)
397
+ var = syms[0]
398
+ var = sp.sympify(var)
399
+ natural = function_domain(expr, var)
400
+ assumptions = combine_natural_domain_condition(natural, domain, var)
401
+ split = evaluate_parameter_strata(
402
+ assumptions,
403
+ var,
404
+ lambda branch_domain: _function_sign(expr, var, branch_domain),
405
+ source="sign-parameter-stratification",
406
+ )
407
+ s = split if split is not None else _function_sign(expr, var, assumptions)
408
+ if strict_inequalities:
409
+ return s
410
+
411
+ # Weak query can safely weaken strict classifications, including strata.
412
+ def weaken(v):
413
+ if v is Sign.POSITIVE:
414
+ return Sign.NONNEGATIVE
415
+ if v is Sign.NEGATIVE:
416
+ return Sign.NONPOSITIVE
417
+ return v
418
+
419
+ if isinstance(s, PropertyResult):
420
+ return normalize_cases(
421
+ (ConditionalValue(weaken(c.value), c.condition) for c in s.cases),
422
+ primary=weaken(s.primary) if s.primary is not None else None,
423
+ source=s.source,
424
+ evidence=s.evidence,
425
+ )
426
+ return weaken(s)
427
+
428
+
429
+ def _sign_implications(value):
430
+ """Return weak sign properties implied by an exact sign class."""
431
+ if value is Sign.POSITIVE:
432
+ return (Sign.NONNEGATIVE,)
433
+ if value is Sign.NEGATIVE:
434
+ return (Sign.NONPOSITIVE,)
435
+ if value is Sign.ZERO:
436
+ return (Sign.NONNEGATIVE, Sign.NONPOSITIVE)
437
+ return ()
438
+
439
+
440
+ def function_sign(
441
+ expr, var=None, *, domain=True, strict_inequalities=True, all_properties=False
442
+ ):
443
+ """Classify sign, optionally returning weaker implied sign properties."""
444
+ expr = sp.sympify(expr)
445
+ if var is None:
446
+ syms = tuple(expr.free_symbols)
447
+ if len(syms) != 1:
448
+ result = _constant_sign(expr)
449
+ return (
450
+ PropertyResult(
451
+ result,
452
+ all_properties=_sign_implications(result),
453
+ source="constant-sign",
454
+ )
455
+ if all_properties
456
+ else result
457
+ )
458
+ var = syms[0]
459
+ from ..proof_cache import property_proof_context
460
+
461
+ result = property_proof_context(expr, var, domain=domain).sign(
462
+ strict_inequalities=strict_inequalities
463
+ )
464
+ if not all_properties:
465
+ return result
466
+ if isinstance(result, PropertyResult):
467
+ return PropertyResult(
468
+ result.primary,
469
+ cases=result.cases,
470
+ all_properties=_sign_implications(result.primary),
471
+ source=result.source,
472
+ exceptions=result.exceptions,
473
+ evidence=result.evidence,
474
+ )
475
+ return PropertyResult(
476
+ result, all_properties=_sign_implications(result), source="sign"
477
+ )
478
+
479
+
480
+ def prove_relation(relation, var=None, *, domain=True):
481
+ """Prove a scalar real relation by reducing it to the shared sign engine.
482
+
483
+ Equality and order relations are normalized to the sign of ``lhs-rhs``.
484
+ Conditional parameter results are preserved rather than collapsed to an
485
+ unknown Boolean.
486
+ """
487
+ relation = sp.sympify(relation)
488
+ if relation in (S.true, S.false):
489
+ return TruthValue.TRUE if relation is S.true else TruthValue.FALSE
490
+ if not isinstance(relation, sp.core.relational.Relational):
491
+ raise TypeError("relation must be a SymPy equality or inequality")
492
+ expr = sp.simplify(relation.lhs - relation.rhs)
493
+ sign = function_sign(expr, var, domain=domain)
494
+
495
+ def classify(value):
496
+ if isinstance(relation, sp.StrictGreaterThan):
497
+ return (
498
+ TruthValue.TRUE
499
+ if value is Sign.POSITIVE
500
+ else (
501
+ TruthValue.FALSE
502
+ if value in {Sign.ZERO, Sign.NONPOSITIVE, Sign.NEGATIVE}
503
+ else TruthValue.UNKNOWN
504
+ )
505
+ )
506
+ if isinstance(relation, sp.GreaterThan):
507
+ return (
508
+ TruthValue.TRUE
509
+ if value in {Sign.POSITIVE, Sign.NONNEGATIVE, Sign.ZERO}
510
+ else (
511
+ TruthValue.FALSE if value is Sign.NEGATIVE else TruthValue.UNKNOWN
512
+ )
513
+ )
514
+ if isinstance(relation, sp.StrictLessThan):
515
+ return (
516
+ TruthValue.TRUE
517
+ if value is Sign.NEGATIVE
518
+ else (
519
+ TruthValue.FALSE
520
+ if value in {Sign.ZERO, Sign.NONNEGATIVE, Sign.POSITIVE}
521
+ else TruthValue.UNKNOWN
522
+ )
523
+ )
524
+ if isinstance(relation, sp.LessThan):
525
+ return (
526
+ TruthValue.TRUE
527
+ if value in {Sign.NEGATIVE, Sign.NONPOSITIVE, Sign.ZERO}
528
+ else (
529
+ TruthValue.FALSE if value is Sign.POSITIVE else TruthValue.UNKNOWN
530
+ )
531
+ )
532
+ if isinstance(relation, sp.Equality):
533
+ return (
534
+ TruthValue.TRUE
535
+ if value is Sign.ZERO
536
+ else (
537
+ TruthValue.FALSE
538
+ if value in {Sign.POSITIVE, Sign.NEGATIVE, Sign.SIGN_CHANGING}
539
+ else TruthValue.UNKNOWN
540
+ )
541
+ )
542
+ if isinstance(relation, sp.Unequality):
543
+ return (
544
+ TruthValue.FALSE
545
+ if value is Sign.ZERO
546
+ else (
547
+ TruthValue.TRUE
548
+ if value in {Sign.POSITIVE, Sign.NEGATIVE}
549
+ else TruthValue.UNKNOWN
550
+ )
551
+ )
552
+ return TruthValue.UNKNOWN
553
+
554
+ if isinstance(sign, PropertyResult):
555
+ return normalize_cases(
556
+ (ConditionalValue(classify(c.value), c.condition) for c in sign.cases),
557
+ primary=classify(sign.primary) if sign.primary is not None else None,
558
+ source="relation-from-sign",
559
+ evidence=sign.evidence,
560
+ )
561
+ return classify(sign)