satassume 0.0.1__tar.gz

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 (95) hide show
  1. satassume-0.0.1/LICENSE +29 -0
  2. satassume-0.0.1/PKG-INFO +352 -0
  3. satassume-0.0.1/README.md +320 -0
  4. satassume-0.0.1/pyproject.toml +47 -0
  5. satassume-0.0.1/satassume/__init__.py +9 -0
  6. satassume-0.0.1/satassume/compile.py +240 -0
  7. satassume-0.0.1/satassume/constfield.py +1441 -0
  8. satassume-0.0.1/satassume/engine.py +1965 -0
  9. satassume-0.0.1/satassume/epoch.py +46 -0
  10. satassume-0.0.1/satassume/euf.py +516 -0
  11. satassume-0.0.1/satassume/euf_adapter.py +260 -0
  12. satassume-0.0.1/satassume/extensions.py +181 -0
  13. satassume-0.0.1/satassume/formula.py +136 -0
  14. satassume-0.0.1/satassume/lra.py +1491 -0
  15. satassume-0.0.1/satassume/lra_adapter.py +741 -0
  16. satassume-0.0.1/satassume/memos.py +232 -0
  17. satassume-0.0.1/satassume/ref.py +579 -0
  18. satassume-0.0.1/satassume/relations.py +1996 -0
  19. satassume-0.0.1/satassume/rules.py +229 -0
  20. satassume-0.0.1/satassume/scope.py +232 -0
  21. satassume-0.0.1/satassume/solver.py +2798 -0
  22. satassume-0.0.1/satassume/sympy_api.py +1071 -0
  23. satassume-0.0.1/satassume/templates/__init__.py +36 -0
  24. satassume-0.0.1/satassume/templates/_common.py +291 -0
  25. satassume-0.0.1/satassume/templates/atoms.py +107 -0
  26. satassume-0.0.1/satassume/templates/core.py +924 -0
  27. satassume-0.0.1/satassume/templates/functions.py +557 -0
  28. satassume-0.0.1/satassume/templates/registry.py +126 -0
  29. satassume-0.0.1/satassume/templates/table.py +253 -0
  30. satassume-0.0.1/satassume/theory.py +225 -0
  31. satassume-0.0.1/satassume/transfer.py +541 -0
  32. satassume-0.0.1/satassume.egg-info/PKG-INFO +352 -0
  33. satassume-0.0.1/satassume.egg-info/SOURCES.txt +93 -0
  34. satassume-0.0.1/satassume.egg-info/dependency_links.txt +1 -0
  35. satassume-0.0.1/satassume.egg-info/requires.txt +8 -0
  36. satassume-0.0.1/satassume.egg-info/top_level.txt +1 -0
  37. satassume-0.0.1/setup.cfg +4 -0
  38. satassume-0.0.1/tests/test_budget_cone.py +206 -0
  39. satassume-0.0.1/tests/test_constfield.py +852 -0
  40. satassume-0.0.1/tests/test_engine.py +210 -0
  41. satassume-0.0.1/tests/test_eq_canonical.py +104 -0
  42. satassume-0.0.1/tests/test_eq_links.py +222 -0
  43. satassume-0.0.1/tests/test_euf.py +988 -0
  44. satassume-0.0.1/tests/test_euf_adapter.py +607 -0
  45. satassume-0.0.1/tests/test_euf_fuzz.py +882 -0
  46. satassume-0.0.1/tests/test_extended_order.py +332 -0
  47. satassume-0.0.1/tests/test_extensibility.py +229 -0
  48. satassume-0.0.1/tests/test_history.py +780 -0
  49. satassume-0.0.1/tests/test_invariant_repros.py +296 -0
  50. satassume-0.0.1/tests/test_invariants.py +307 -0
  51. satassume-0.0.1/tests/test_known_gaps.py +49 -0
  52. satassume-0.0.1/tests/test_lazy_atoms.py +53 -0
  53. satassume-0.0.1/tests/test_lra.py +1330 -0
  54. satassume-0.0.1/tests/test_lra_adapter.py +557 -0
  55. satassume-0.0.1/tests/test_lra_constant_coefficients.py +518 -0
  56. satassume-0.0.1/tests/test_lra_constants.py +403 -0
  57. satassume-0.0.1/tests/test_lra_exhaustion.py +320 -0
  58. satassume-0.0.1/tests/test_lra_fuzz.py +385 -0
  59. satassume-0.0.1/tests/test_lra_integers.py +553 -0
  60. satassume-0.0.1/tests/test_memos.py +156 -0
  61. satassume-0.0.1/tests/test_noncommutative.py +457 -0
  62. satassume-0.0.1/tests/test_p2_review.py +72 -0
  63. satassume-0.0.1/tests/test_p3_review.py +85 -0
  64. satassume-0.0.1/tests/test_p3_review2.py +30 -0
  65. satassume-0.0.1/tests/test_p4_review.py +71 -0
  66. satassume-0.0.1/tests/test_p5b_review.py +60 -0
  67. satassume-0.0.1/tests/test_p6_review.py +33 -0
  68. satassume-0.0.1/tests/test_p7_review_fixed_invariant_repros_replay.py +24 -0
  69. satassume-0.0.1/tests/test_prover_gaps.py +113 -0
  70. satassume-0.0.1/tests/test_ref.py +326 -0
  71. satassume-0.0.1/tests/test_registry_versioning.py +609 -0
  72. satassume-0.0.1/tests/test_relations.py +429 -0
  73. satassume-0.0.1/tests/test_relevance.py +429 -0
  74. satassume-0.0.1/tests/test_rules.py +52 -0
  75. satassume-0.0.1/tests/test_scope.py +218 -0
  76. satassume-0.0.1/tests/test_set_verdict.py +204 -0
  77. satassume-0.0.1/tests/test_shared_facts.py +161 -0
  78. satassume-0.0.1/tests/test_solver.py +1023 -0
  79. satassume-0.0.1/tests/test_solver_incremental.py +1136 -0
  80. satassume-0.0.1/tests/test_solver_real_theories.py +47 -0
  81. satassume-0.0.1/tests/test_switched_glue.py +141 -0
  82. satassume-0.0.1/tests/test_sympy_api.py +406 -0
  83. satassume-0.0.1/tests/test_template_tables.py +224 -0
  84. satassume-0.0.1/tests/test_template_volume.py +40 -0
  85. satassume-0.0.1/tests/test_templates.py +597 -0
  86. satassume-0.0.1/tests/test_theory_hooks.py +233 -0
  87. satassume-0.0.1/tests/test_total_templates.py +58 -0
  88. satassume-0.0.1/tests/test_totality.py +104 -0
  89. satassume-0.0.1/tests/test_transfer.py +241 -0
  90. satassume-0.0.1/tests/test_transfer_fuzz.py +230 -0
  91. satassume-0.0.1/tests/test_transfer_numbers.py +33 -0
  92. satassume-0.0.1/tests/test_verify_integration.py +222 -0
  93. satassume-0.0.1/tests/test_verify_soundness.py +481 -0
  94. satassume-0.0.1/tests/test_writeback_provenance.py +538 -0
  95. satassume-0.0.1/tests/test_zero_glue.py +210 -0
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, satassume contributors
4
+ Portions copied from SymPy, Copyright (c) 2006-2026 SymPy Development Team
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,352 @@
1
+ Metadata-Version: 2.4
2
+ Name: satassume
3
+ Version: 0.0.1
4
+ Summary: An incremental SAT-based engine for SymPy's ask(): unary scalar predicates on scalar expressions
5
+ Author: satassume contributors
6
+ License: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/tilorc-bot/satassume
8
+ Project-URL: Source, https://github.com/tilorc-bot/satassume
9
+ Project-URL: Issues, https://github.com/tilorc-bot/satassume/issues
10
+ Project-URL: Documentation, https://github.com/tilorc-bot/satassume/tree/main/docs
11
+ Keywords: sympy,assumptions,sat,cdcl,smt,symbolic
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: BSD License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: sympy
26
+ Requires-Dist: sympy>=1.12; extra == "sympy"
27
+ Requires-Dist: mpmath; extra == "sympy"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest; extra == "dev"
30
+ Requires-Dist: hypothesis; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # satassume
34
+
35
+ A SAT-based engine for SymPy's `ask(proposition, assumptions)`, currently
36
+ scoped to exactly one slice: **unary scalar predicates on scalar
37
+ expressions**, answered purely by the SAT engine. Propositions and
38
+ assumptions are Boolean combinations of `Q.<name>(expr)` where `name` is in
39
+ the vocabulary of `satassume/rules.py` (`integer`, `real`, `positive`,
40
+ `prime`, `hermitian`, ...) and `expr` is a scalar `Expr`. Everything is
41
+ answered from one rule base, one set of structural templates and one
42
+ incremental CDCL solver; the engine never consults SymPy's `_eval_is_*`
43
+ handlers or SymPy's own `ask`/`satask`.
44
+
45
+ Custom predicates are in scope once a clause-generating function is
46
+ registered for them with `satassume.register(pred, *classes)`, the
47
+ counterpart of SymPy's `Predicate.register` (see
48
+ `satassume/extensions.py`); a registered vocabulary predicate on a new
49
+ class makes objects of that class ordinary nodes.
50
+
51
+ Relations (`Q.eq/ne/lt/le/gt/ge`, `Eq`, `x < 0`, `Q.is_true(x < 0)`) are
52
+ being added through theory solvers on the CDCL solver (DPLL(T), LRA and EUF;
53
+ see `satassume/relations.py` and [docs/theories.md](docs/theories.md)).
54
+ A relation in the assumptions that no adapter interprets (a Float or
55
+ `AccumBounds` bound) is kept as a free Boolean atom by default, so the rest
56
+ of the assumptions still answers (`Q.real(m)` under
57
+ `Q.odd(m) & Q.ge(m, 1.5)` is True); this only drops what the relation
58
+ says. `Engine(uninterpreted="none")` is the old behaviour, opt-in: `ask`
59
+ returns None.
60
+ Order relations are over the extended reals and assert that their sides
61
+ are extended reals (`x < 1` implies `Q.extended_real(x)`, `x < oo` is
62
+ `x` extended real and not `+oo`, `x < I` is false); `Eq`/`Ne` compare
63
+ values in any domain and assert nothing about the sides.
64
+ Out of scope for now: matrix predicates and matrix arguments, unregistered
65
+ custom predicates, and replacing the old `expr.is_*` system. As a
66
+ proposition such a predicate gives None; as a conjunct of the assumptions
67
+ it is an opaque atom (a free Boolean nothing else reads), so it no longer
68
+ sinks the rest: `Q.real(x)` under `Q.positive(x) & Q.invertible(M)` is
69
+ True, and `Q.invertible(M) & ~Q.invertible(M)` raises like any
70
+ inconsistent set.
71
+
72
+ **Routing rule.** Any out-of-scope query makes `ask` return `None` without
73
+ touching the engine. Relations are the exception once theory adapters are
74
+ present (the default when `satassume/lra_adapter.py` and
75
+ `satassume/euf_adapter.py` exist): they reach the engine, and `ask` returns
76
+ None only when no theory interprets one of them; `out_of_scope` still
77
+ reports them as `relation`. That is scoping, not a fallback: the caller (SymPy's
78
+ `ask`) is expected to route such inputs to its existing path (`satask`, the
79
+ LRA theory, the matrix handlers). `out_of_scope(prop, assumptions)` reports
80
+ the category (`relation`, `matrix`, `custom`, `other`) so the caller can
81
+ decide before asking. The engine itself contains no fallback to SymPy's old
82
+ handlers or to `satask`; adding one would defeat the purpose.
83
+
84
+ **Definition of done for this slice.**
85
+
86
+ 1. Every in-scope query in the recorded corpus (`queries.jsonl`, recorded
87
+ from SymPy's own tests) is answered identically to SymPy or better (a
88
+ definite answer where SymPy returns None), with zero contradictions;
89
+ 2. faster than `sympy.ask` on each in-scope query;
90
+ 3. `Predicate.register` kept as a way to add clause-generating functions,
91
+ so SymPy's extensibility tests keep passing.
92
+
93
+ Where it stands: 1 is met except for 70 records where SymPy's answer is
94
+ wrong for a concrete value or needs reasoning outside the templates (listed
95
+ under Results); 2 is met on 2532 of 2583 compared records; 3 is met by
96
+ `satassume.register`, with the hook inside SymPy's own `Predicate.register`
97
+ left for the landing step.
98
+
99
+ **Long-term goal.** Replacing the old per-object `expr.is_*` system with the
100
+ same engine remains the goal; it is deferred until this
101
+ slice is finished. `Engine.is_` exists because the engine uses it internally
102
+ for context-free queries and the corpus tools replay old-system records
103
+ through it for information, but nothing here hooks it into SymPy. See
104
+ [PLAN.md](PLAN.md).
105
+
106
+ ## Installation
107
+
108
+ ```
109
+ pip install "satassume[sympy]"
110
+ ```
111
+
112
+ The core engine (`satassume`) has no dependencies; `satassume.sympy_api`
113
+ and the templates need SymPy, which the `sympy` extra installs. This is an
114
+ early, experimental release: the API may change between versions.
115
+
116
+ ## Usage
117
+
118
+ ```python
119
+ from sympy import Symbol, Q, exp
120
+ from satassume.sympy_api import ask, out_of_scope
121
+
122
+ y = Symbol('y')
123
+ ask(Q.positive(exp(y)), Q.real(y)) # True
124
+ ask(Q.even(y + 1), Q.odd(y)) # True
125
+ ask(Q.positive(y), Q.real(y)) # None: undecided, in scope
126
+ ask(Q.positive(y), Q.gt(y, 0)) # None: y = oo satisfies y > 0 (relations are over the extended reals), and positive means finite
127
+ ask(Q.extended_positive(y), Q.gt(y, 0)) # True: y > 0 makes y an extended real
128
+ ask(Q.positive(y), Q.gt(y, 0) & Q.real(y)) # True (LRA theory)
129
+ ask(Q.integer(y), Q.gt(y, 0) & Q.lt(y, 1)) # False (integrality in LRA: bounds rounded, branch and bound)
130
+ from sympy import S, pi
131
+ ask(Q.integer(y/pi + S.Half), Q.gt(y, -pi/2) & Q.lt(y, pi/2)) # False (exact pi coefficients, satassume/constfield.py)
132
+ out_of_scope(Q.positive(y), Q.gt(y, 0)) # 'relation' (answered anyway when adapters are present)
133
+
134
+ from sympy import Integer, Predicate, log
135
+ from satassume import register, P, Implies
136
+ @register('mersenne', Integer) # the counterpart of Q.mersenne.register(Integer)
137
+ def _(n):
138
+ return Implies(P('integer', log(n + 1, 2)), P('mersenne', n))
139
+ ask(Predicate('mersenne')(Integer(31))) # True
140
+ ```
141
+
142
+ ## How it answers
143
+
144
+ Both SymPy assumption systems are propositional reasoning over the same
145
+ predicate vocabulary (`integer -> rational -> real -> complex`, `real ==
146
+ negative | zero | positive`, ...) plus structural knowledge about expression
147
+ classes. satassume keeps one rule base, one incremental CDCL solver per
148
+ session, and a cache of its own (`DictCache`, keyed by node) for the
149
+ context-free facts it derives. It never reads or writes SymPy's per-object
150
+ `_assumptions`: those hold whatever SymPy's `_eval_is_*` handlers cached,
151
+ which can be wrong (`(0**n).is_finite` is True for a plain `n`), and a
152
+ `Symbol`'s `_assumptions` is one fact base shared by every symbol with the
153
+ same assumptions. SymPy objects enter only through the templates: the
154
+ assumptions a symbol was declared with, and the properties of fixed-value
155
+ constants. Assumptions enter the solver as solver assumptions under
156
+ a selector literal and never touch the cache; the session is reused while
157
+ the assumptions stay the same. Everything kept between queries (the fact
158
+ caches, the reused sessions, the answer and split memos) records the registry
159
+ epoch it was filled under, a process-wide counter that every registration or
160
+ unregistration of a clause-generating function, every template registration
161
+ and every assignment of an engine's `extensions` or `relation_specs` bumps,
162
+ and is dropped at the next query when the epoch has moved on; a session that a
163
+ query made raise is dropped with the raise. So an answer is a function of the
164
+ query, the assumptions, the engine's configuration and the registrations in
165
+ force, never of earlier queries. Discovery in a fresh session visits only the
166
+ cone of the query (a reused session may also escalate what earlier queries
167
+ left, which changes the cost, never the answer), root-level propagation decides most queries, and search runs
168
+ only when propagation is inconclusive.
169
+
170
+ ## Layout
171
+
172
+ | Path | What |
173
+ |---|---|
174
+ | `satassume/rules.py` | the single rule base and predicate vocabulary, in the old system's string syntax |
175
+ | `satassume/formula.py`, `compile.py` | atoms `P(pred, expr)`, formulas, clause compilation |
176
+ | `satassume/solver.py` | incremental CDCL with assumptions, root-level propagation, `entails` |
177
+ | `satassume/engine.py` | sessions, discovery, caching |
178
+ | `satassume/templates/` | structural rules per SymPy class |
179
+ | `satassume/sympy_api.py` | `ask`, `out_of_scope`, `to_formula`, `Unsupported` |
180
+ | `satassume/constfield.py` | exact numbers in `Q(pi, E, sqrt(2), ...)` for LRA coefficients and bounds (signs by interval refinement, `Undecided` when out of reach) |
181
+ | `satassume/extensions.py` | `register(pred, *classes)`: clause-generating functions for custom predicates and for vocabulary predicates on new classes |
182
+ | `tools/record_queries.py` | pytest plugin recording every query SymPy's tests make |
183
+ | `tools/compare.py` | replay a recorded corpus, classified in scope / out of scope, and report agreement |
184
+ | `tools/bench.py` | contextual `ask` microbenchmarks, SymPy versus satassume |
185
+ | `tools/totality.py` | the totality gate: every node block of the templates must be satisfiable for every assignment of its children the rule base and their own blocks allow (`tests/test_totality.py` runs it in CI; `--corpus`/`--stream` for the long mode) |
186
+ | `benchmarks/counters.py` | asv suite: per-commit counts of what the engine builds and does (nodes, clauses, rule blocks, propagations, ...) and the refine-stream time |
187
+ | `benchmarks/memory.py` | asv suite: peak RSS of a stream pass, and its traced Python allocations (peak, and retained afterwards) |
188
+ | `docs/` | [design](docs/design.md) (engine, semantic decisions, relevance), [theories](docs/theories.md) (relations, LRA, EUF, transfer), [performance](docs/performance.md) (what landed, what was dropped, invariants), [testing](docs/testing.md) (suite, gates, fuzzers, asv), [agents](docs/agents.md) (rules for coding agents) |
189
+
190
+ The dated agent reports that preceded `docs/` (2026-09-23 to 26) are in the
191
+ tag `agent-reports-2026-09`.
192
+
193
+ ## Running
194
+
195
+ ```bash
196
+ # unit tests (no SymPy needed for solver/rules; templates and the API need SymPy)
197
+ PYTHONPATH=.:/path/to/sympy uv run --no-project --with pytest --with mpmath python -m pytest -q tests
198
+
199
+ # record SymPy's own queries (only if queries.jsonl is missing), then replay them
200
+ cd /path/to/sympy
201
+ RECORD_OUT=/path/to/satassume/queries.jsonl PYTHONPATH=/path/to/satassume/tools:. \
202
+ python -m pytest -p record_queries -p no:cacheprovider sympy/assumptions/tests sympy/core/tests/test_assumptions.py
203
+ cd /path/to/satassume
204
+ PYTHONPATH=.:/path/to/sympy python tools/compare.py queries.jsonl --in-scope-only --time-sympy
205
+
206
+ # microbenchmarks
207
+ PYTHONPATH=.:/path/to/sympy python tools/bench.py
208
+
209
+ # asv: counters and stream time per commit of main (SymPy from $SATASSUME_SYMPY,
210
+ # stream from $SATASSUME_STREAM, as for tools/ab.py); results in .asv/
211
+ pip install asv virtualenv
212
+ asv run HASHFILE:<(git rev-list --first-parent -n 20 main)
213
+ asv publish && asv preview
214
+ # time against a count on two y-axes, and change per commit: serve
215
+ # .asv/site (index.html -> benchmarks/compare.html, asv -> ../html)
216
+ ```
217
+
218
+ The asv counters are exact under the fixed `PYTHONHASHSEED` in
219
+ `asv.conf.json`, so they show a change in what a commit instantiates or
220
+ searches without the noise of a timing; they are not costs (see the module
221
+ docstring).
222
+
223
+ `tools/compare.py` exits nonzero only when a definite answer contradicts
224
+ SymPy on an in-scope record. Without `--in-scope-only` it also replays the
225
+ out-of-scope and old-system records and reports them as informational.
226
+
227
+ ## Results (corpus `queries.jsonl`, 9230 records, `tools/compare.py`)
228
+
229
+ In-scope new-system records (`ask` and `_ask_recursive` calls from
230
+ `sympy/assumptions/tests` and `sympy/core/tests/test_assumptions.py`):
231
+
232
+ | In-scope records | Agree | Extra answers | None where SymPy answered | Wrong | Raises where SymPy answered |
233
+ |---|---|---|---|---|---|
234
+ | 2588 (7 more unreplayable) | 2497 (96.5%) | 16 | 70 | 0 | 5 |
235
+
236
+ "Extra answers" are definite answers where SymPy returned None; each one
237
+ was checked by hand. The five "raises" are one semantic choice: the engine
238
+ treats an assumption contradicting a symbol's declared facts
239
+ (`ask(Q.commutative(x), ~Q.commutative(x))`) as inconsistent and raises
240
+ `ValueError`, where SymPy's handler path trusts the assumption.
241
+
242
+ The 70 misses are deliberate. For 67 of them SymPy's answer is false for a
243
+ value that satisfies the assumptions, so no sound rule can reproduce it:
244
+
245
+ * a zero argument (26): `Q.imaginary(I*x)` and `Q.imaginary(x*y)` under
246
+ `Q.real` (the product is 0 for `x = 0`), `Q.imaginary(x + y)` and
247
+ `Q.imaginary(x + I)` under an imaginary and a real term (the real term
248
+ may be 0, or the imaginary terms may cancel: `I - I`), the matching
249
+ `hermitian`/`antihermitian` records, `Q.integer(sqrt(2)*x)` for integer
250
+ `x`, `Q.imaginary(x**y)` for negative `x` and `Q.integer(2*y)` (which
251
+ includes integer `y`), `Q.nonzero(5**(2*I*pi*n))` for integer `n`;
252
+ * an infinite argument (37): `re`, `im`, `Abs`, `exp`, `sin` and `cos`
253
+ are declared complex or finite by SymPy for every argument, but
254
+ `re(oo) = oo`, `sin(oo*I) = oo*I`, `exp(oo) = oo`; `Q.finite(log(x))` for
255
+ nonzero `x` (`log(oo)`), `Q.finite(2**x)` False for infinite `x`
256
+ (`2**-oo = 0`), `Q.complex(x**y)` and `Q.algebraic(x**y)` for complex or
257
+ algebraic `x` (`0**-1 = zoo`), `Q.finite(x*y)` for zero `y` and infinite
258
+ `x` (`0*oo = nan`);
259
+ * four more: `Q.positive(acot(x))` for real `x` (`acot(-1) = -pi/4`),
260
+ `Q.positive(acos(x))` on `[-1, 1]` (`acos(1) = 0`), and
261
+ `Q.imaginary((2*I)**x)` False for imaginary `x` (true for
262
+ `x = I*pi/(2*log(2))`).
263
+
264
+ The other three need reasoning the templates do not do: `(3*I)**I` and
265
+ `(1 + I)**I` are not real because `|b| != 1`, which needs `Abs` of a
266
+ non-atomic base; the primality of `cos(1)**2 + sin(1)**2 + 1234...` needs a
267
+ trigonometric identity.
268
+
269
+ Out-of-scope records, returned as None by rule (informational; relations
270
+ are answered by the theories, `tools/compare.py --relations-only`):
271
+
272
+ | Category | Records | SymPy also None | SymPy answered |
273
+ |---|---|---|---|
274
+ | relations | 78 | 15 | 63; with the LRA and EUF theories 75 of the 78 agree, 3 None, 0 wrong |
275
+ | matrix predicates or non-scalar arguments | 189 | 29 | 160 |
276
+ | custom predicates | 0 | | |
277
+ | not a Boolean proposition | 8 | 5 | 1 (SymPy raised on 2) |
278
+
279
+ Old-system `expr.is_*` records, replayed through `Engine.is_` (out of
280
+ scope, informational): 6343 replayable, 6033 agree (95.1%), 27 extra
281
+ answers, 283 None where SymPy answered, 0 wrong. (Before the engine
282
+ stopped reading SymPy's cached `_assumptions` it was 6041 / 30 / 272: part
283
+ of that agreement was SymPy's own cached answers read back.)
284
+
285
+ Time (`tools/compare.py --in-scope-only --time-sympy`, both sides in the
286
+ same process, garbage collection frozen and disabled inside the timed
287
+ calls, `sympy.ask` timed on an evaluated rebuild when it raises on the
288
+ unevaluated one and left out of the comparison if it still raises, 5
289
+ records): satassume takes 1.19 s for the in-scope records against 6.88 s
290
+ for `sympy.ask` on the 2583 compared records, and is slower on 51 of them
291
+ (20 by more than 0.3 ms). Those are undecided queries that need the full
292
+ instantiation of a `Pow` cone (six nodes with the derived `2*e`, `b - 1`,
293
+ `b + 1`) and two CDCL solves, at 1.5 to 2.7 ms against 0.9 to 1.8 ms for
294
+ SymPy, and constants asked about for the first time (their old-system
295
+ properties are read once). "Faster on each" is therefore met on 98% of
296
+ the records, not on all; the worst five: `Q.imaginary((2*I)**x) |
297
+ Q.imaginary(x)` 2.66 ms vs 1.76, `Q.nonzero(5**(2*I*pi*n)) | Q.integer(n)`
298
+ 2.44 vs 1.59, `Q.complex(x**y) | Q.complex(x) & Q.complex(y)` 2.20 vs
299
+ 0.85, `Q.real(x**(y/z)) | Q.positive(x) & Q.real(x) & Q.real(y/z)` 2.08 vs
300
+ 1.33, `Q.imaginary(x**y) | Q.negative(x) & Q.rational(y) & Q.integer(2*y)`
301
+ 2.02 vs 1.22.
302
+
303
+ Microbenchmarks (`tools/bench.py`, pure Python, this machine, 200 repetitions
304
+ per case, both sides return the same answer):
305
+
306
+ | Query | `sympy.ask` | satassume |
307
+ |---|---|---|
308
+ | `Q.positive(y + 1) \| Q.positive(y)` | 378 us | 52 us |
309
+ | `Q.zero(w*y) \| Q.finite(w) & Q.zero(y)` | 463 us | 74 us |
310
+ | `Q.real(w*y) \| Q.real(w) & Q.real(y)` | 343 us | 43 us |
311
+ | `Q.even(y + 1) \| Q.odd(y)` | 309 us | 44 us |
312
+ | `Q.positive(exp(y)) \| Q.real(y)` | 268 us | 39 us |
313
+ | `Q.positive(((y**2 + 1)**w)**2) \| Q.real(w) & Q.real(y)` | 1115 us | 134 us |
314
+ | `Q.positive(w**2 + y + z) \| Q.nonnegative(z) & Q.positive(y) & Q.real(w)` | 1111 us | 96 us |
315
+ | `Q.negative(y) \| Q.positive(y) \| Q.nonzero(y) & Q.real(y)` | 1271 us | 32 us |
316
+
317
+ ## Known gaps
318
+
319
+ Queries answered None where a definite answer holds for every value the
320
+ assumptions allow. None of them is a wrong answer. Each one is pinned as a
321
+ strict xfail in `tests/test_known_gaps.py`, so closing a gap fails that test
322
+ until the case is moved and this list updated.
323
+
324
+ Tracked in an issue:
325
+
326
+ * differences whose sides are not known to be real (`Q.negative(a - b)`
327
+ under `Q.positive(b - a)`, `Q.zero(a - b)` under `Q.eq(a, b)` with finite
328
+ sides): #42;
329
+ * integral `re(x)` and `im(x)` do not make `x` finite: #19.
330
+
331
+ Not tracked, because neither SymPy system answers them either (except the
332
+ first, which SymPy's `ask` gets by substituting the zero symbol) and no
333
+ caller has needed them:
334
+
335
+ * `Q.integer(1/(m + 1))` under `Q.zero(m)`: there is no "equals 1" fact, and
336
+ a symbol pinned to a constant is not substituted;
337
+ * `Q.eq(f(x), f(pi))` under `Q.eq(2*x, 2*pi)`: a constant term gets no
338
+ interface equality in the theory combination;
339
+ * `Q.eq(x, 3)` under `Q.eq(x, log(8)/log(2))`: one value written two ways is
340
+ two unrelated constants;
341
+ * `Q.positive(x)` under `Q.gt(x, pi**-(10**20))`: a tiny constant is not
342
+ shown positive.
343
+
344
+ Deliberately not read: Float bounds. SymPy compares `Float(0.1) > 1/10`
345
+ exactly but `Eq(Float(0.1), 1/10)` at the Float's precision, so there is no
346
+ single right reading, and a Float bound is left to the uninterpreted path
347
+ (a free atom by default).
348
+
349
+ ## License
350
+
351
+ BSD-3-Clause. The rule strings and several structural rules are copied or
352
+ adapted from SymPy (BSD).