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.
- satassume-0.0.1/LICENSE +29 -0
- satassume-0.0.1/PKG-INFO +352 -0
- satassume-0.0.1/README.md +320 -0
- satassume-0.0.1/pyproject.toml +47 -0
- satassume-0.0.1/satassume/__init__.py +9 -0
- satassume-0.0.1/satassume/compile.py +240 -0
- satassume-0.0.1/satassume/constfield.py +1441 -0
- satassume-0.0.1/satassume/engine.py +1965 -0
- satassume-0.0.1/satassume/epoch.py +46 -0
- satassume-0.0.1/satassume/euf.py +516 -0
- satassume-0.0.1/satassume/euf_adapter.py +260 -0
- satassume-0.0.1/satassume/extensions.py +181 -0
- satassume-0.0.1/satassume/formula.py +136 -0
- satassume-0.0.1/satassume/lra.py +1491 -0
- satassume-0.0.1/satassume/lra_adapter.py +741 -0
- satassume-0.0.1/satassume/memos.py +232 -0
- satassume-0.0.1/satassume/ref.py +579 -0
- satassume-0.0.1/satassume/relations.py +1996 -0
- satassume-0.0.1/satassume/rules.py +229 -0
- satassume-0.0.1/satassume/scope.py +232 -0
- satassume-0.0.1/satassume/solver.py +2798 -0
- satassume-0.0.1/satassume/sympy_api.py +1071 -0
- satassume-0.0.1/satassume/templates/__init__.py +36 -0
- satassume-0.0.1/satassume/templates/_common.py +291 -0
- satassume-0.0.1/satassume/templates/atoms.py +107 -0
- satassume-0.0.1/satassume/templates/core.py +924 -0
- satassume-0.0.1/satassume/templates/functions.py +557 -0
- satassume-0.0.1/satassume/templates/registry.py +126 -0
- satassume-0.0.1/satassume/templates/table.py +253 -0
- satassume-0.0.1/satassume/theory.py +225 -0
- satassume-0.0.1/satassume/transfer.py +541 -0
- satassume-0.0.1/satassume.egg-info/PKG-INFO +352 -0
- satassume-0.0.1/satassume.egg-info/SOURCES.txt +93 -0
- satassume-0.0.1/satassume.egg-info/dependency_links.txt +1 -0
- satassume-0.0.1/satassume.egg-info/requires.txt +8 -0
- satassume-0.0.1/satassume.egg-info/top_level.txt +1 -0
- satassume-0.0.1/setup.cfg +4 -0
- satassume-0.0.1/tests/test_budget_cone.py +206 -0
- satassume-0.0.1/tests/test_constfield.py +852 -0
- satassume-0.0.1/tests/test_engine.py +210 -0
- satassume-0.0.1/tests/test_eq_canonical.py +104 -0
- satassume-0.0.1/tests/test_eq_links.py +222 -0
- satassume-0.0.1/tests/test_euf.py +988 -0
- satassume-0.0.1/tests/test_euf_adapter.py +607 -0
- satassume-0.0.1/tests/test_euf_fuzz.py +882 -0
- satassume-0.0.1/tests/test_extended_order.py +332 -0
- satassume-0.0.1/tests/test_extensibility.py +229 -0
- satassume-0.0.1/tests/test_history.py +780 -0
- satassume-0.0.1/tests/test_invariant_repros.py +296 -0
- satassume-0.0.1/tests/test_invariants.py +307 -0
- satassume-0.0.1/tests/test_known_gaps.py +49 -0
- satassume-0.0.1/tests/test_lazy_atoms.py +53 -0
- satassume-0.0.1/tests/test_lra.py +1330 -0
- satassume-0.0.1/tests/test_lra_adapter.py +557 -0
- satassume-0.0.1/tests/test_lra_constant_coefficients.py +518 -0
- satassume-0.0.1/tests/test_lra_constants.py +403 -0
- satassume-0.0.1/tests/test_lra_exhaustion.py +320 -0
- satassume-0.0.1/tests/test_lra_fuzz.py +385 -0
- satassume-0.0.1/tests/test_lra_integers.py +553 -0
- satassume-0.0.1/tests/test_memos.py +156 -0
- satassume-0.0.1/tests/test_noncommutative.py +457 -0
- satassume-0.0.1/tests/test_p2_review.py +72 -0
- satassume-0.0.1/tests/test_p3_review.py +85 -0
- satassume-0.0.1/tests/test_p3_review2.py +30 -0
- satassume-0.0.1/tests/test_p4_review.py +71 -0
- satassume-0.0.1/tests/test_p5b_review.py +60 -0
- satassume-0.0.1/tests/test_p6_review.py +33 -0
- satassume-0.0.1/tests/test_p7_review_fixed_invariant_repros_replay.py +24 -0
- satassume-0.0.1/tests/test_prover_gaps.py +113 -0
- satassume-0.0.1/tests/test_ref.py +326 -0
- satassume-0.0.1/tests/test_registry_versioning.py +609 -0
- satassume-0.0.1/tests/test_relations.py +429 -0
- satassume-0.0.1/tests/test_relevance.py +429 -0
- satassume-0.0.1/tests/test_rules.py +52 -0
- satassume-0.0.1/tests/test_scope.py +218 -0
- satassume-0.0.1/tests/test_set_verdict.py +204 -0
- satassume-0.0.1/tests/test_shared_facts.py +161 -0
- satassume-0.0.1/tests/test_solver.py +1023 -0
- satassume-0.0.1/tests/test_solver_incremental.py +1136 -0
- satassume-0.0.1/tests/test_solver_real_theories.py +47 -0
- satassume-0.0.1/tests/test_switched_glue.py +141 -0
- satassume-0.0.1/tests/test_sympy_api.py +406 -0
- satassume-0.0.1/tests/test_template_tables.py +224 -0
- satassume-0.0.1/tests/test_template_volume.py +40 -0
- satassume-0.0.1/tests/test_templates.py +597 -0
- satassume-0.0.1/tests/test_theory_hooks.py +233 -0
- satassume-0.0.1/tests/test_total_templates.py +58 -0
- satassume-0.0.1/tests/test_totality.py +104 -0
- satassume-0.0.1/tests/test_transfer.py +241 -0
- satassume-0.0.1/tests/test_transfer_fuzz.py +230 -0
- satassume-0.0.1/tests/test_transfer_numbers.py +33 -0
- satassume-0.0.1/tests/test_verify_integration.py +222 -0
- satassume-0.0.1/tests/test_verify_soundness.py +481 -0
- satassume-0.0.1/tests/test_writeback_provenance.py +538 -0
- satassume-0.0.1/tests/test_zero_glue.py +210 -0
satassume-0.0.1/LICENSE
ADDED
|
@@ -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.
|
satassume-0.0.1/PKG-INFO
ADDED
|
@@ -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).
|