Skip to content

Commit 98221a1

Browse files
Hardcode84claude
andcommitted
Update migration plan for new ixsimpl API (lambdify, has, eval, check)
ixsimpl now covers every sympy feature used in Wave: lambdify() and Expr.eval() replace sympy.lambdify, Expr.has() is native, abs_() is provided, and ctx.check() replaces the sympy.solve entailment check. No hard API gaps remain; end state is full sympy removal. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Ivan Butygin <ivan.butygin@gmail.com>
1 parent e6d9992 commit 98221a1

2 files changed

Lines changed: 42 additions & 41 deletions

File tree

docs/sympy-to-ixsimpl-migration.md

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ Source: `ixsimpl/_ixsimpl.pyi`, `ixsimpl/__init__.py`, `ixsimpl/sympy_conv.py`.
3838
| `.simplify(assumptions=)` | Simplification with assumption constraints |
3939
| `.expand()` | Expression expansion |
4040
| `.to_c()` | C code generation |
41-
| `.free_symbols` | Free symbol set (Python-level, on `Expr` subclass) |
41+
| `.free_symbols` | Free symbol set (cached property on `Expr` subclass) |
42+
| `.has(sym)` | True if `sym` appears in the expression tree (replaces `sympy_expr.has()`) |
43+
| `.eval(env)` | Evaluate with concrete values (replaces `expr.subs(dict)` + `int()`) |
4244
| `.is_error` / `.is_parse_error` / `.is_domain_error` | Error detection |
4345
| Arithmetic operators | `+`, `-`, `*`, `/`, `>=`, `>`, `<=`, `<`, `==`, `!=` |
4446

@@ -50,13 +52,14 @@ Source: `ixsimpl/_ixsimpl.pyi`, `ixsimpl/__init__.py`, `ixsimpl/sympy_conv.py`.
5052
| `ctx.true_()` / `ctx.false_()` | Boolean constants |
5153
| `ctx.eq(a, b)` / `ctx.ne(a, b)` | Equality/inequality nodes |
5254
| `ctx.parse(input)` | Parse expression from string |
55+
| `ctx.check(expr, assumptions=)` | Entailment query: returns `True`/`False`/`None` (replaces some `sympy.solve` uses) |
5356
| `ctx.simplify_batch(exprs, assumptions=)` | Batch simplification |
5457
| `ctx.errors` / `ctx.clear_errors()` | Error reporting |
5558
| `ctx.stats()` / `ctx.stats_reset()` | Performance stats |
5659

5760
### Free functions
58-
`floor`, `ceil`, `mod`, `max_`, `min_`, `xor_`, `and_`, `or_`, `not_`, `pw`,
59-
`same_node`
61+
`floor`, `ceil`, `mod`, `max_`, `min_`, `abs_`, `xor_`, `and_`, `or_`, `not_`,
62+
`pw`, `same_node`, `lambdify`
6063

6164
### Sympy conversion layer (`ixsimpl.sympy_conv`)
6265
`from_sympy(ctx, expr)`, `to_sympy(expr, symbols=, xor_fn=)`,
@@ -79,50 +82,54 @@ name. `Abs` is the only notable missing conversion (emulable via piecewise).
7982
| Category | Sympy API Used | ixsimpl Equivalent | Actual Gap |
8083
|----------|---------------|-------------------|------------|
8184
| **Symbol creation** | `sympy.Symbol(name, integer=True, nonneg=True)` | `ctx.sym(name)` + `extract_assumptions` | **Soft.** ixsimpl symbols carry no assumptions intrinsically; assumptions are passed separately to `simplify()`. Wave would need to track assumptions in a side table or always extract from sympy symbols during conversion. |
82-
| **Expression construction** | `Add`, `Mul`, `Pow`, `Integer`, `Rational`, `Mod(evaluate=False)`, `floor`, `ceiling`, `Min`, `Max`, `Abs`, `Piecewise` | Arithmetic ops, `ctx.int_()`, `ctx.rat()`, `floor`, `ceil`, `mod`, `max_`, `min_`, `pw` | **Soft.** Missing: `Abs` (emulate via `pw`), `evaluate=False` (ixsimpl does not eagerly evaluate, so not needed). `Pow` must be expanded to repeated multiplication (already done in `from_sympy`). |
85+
| **Expression construction** | `Add`, `Mul`, `Pow`, `Integer`, `Rational`, `Mod(evaluate=False)`, `floor`, `ceiling`, `Min`, `Max`, `Abs`, `Piecewise` | Arithmetic ops, `ctx.int_()`, `ctx.rat()`, `floor`, `ceil`, `mod`, `max_`, `min_`, `abs_`, `pw` | **None.** `Abs` via `abs_()` (piecewise under the hood). `evaluate=False` not needed (ixsimpl does not eagerly evaluate). `Pow` expanded to repeated multiplication (already done in `from_sympy`). |
8386
| **Piecewise / conditionals** | `sympy.Piecewise((val, cond), ...)` | `pw(*branches)`, `.pw_ncases`, `.pw_value(i)`, `.pw_cond(i)` | **None.** Full piecewise support. The `piecewise_aware_subs()` workaround in indexing.py exists only because sympy's `.subs()` triggers expensive boolean simplification on Piecewise -- ixsimpl's `.subs()` does not have this problem. |
84-
| **Structural inspection** | `isinstance(expr, sympy.X)`, `.is_number`, `.is_Atom`, `.args`, `.func` | `.tag` (tag-based dispatch), `.nchildren`, `.children`, `.child(i)`, specialized accessors | **Soft.** `isinstance` dispatch maps to `expr.tag == ixsimpl.ADD` etc. Specialized accessors (`.add_nterms`, `.mul_nfactors`) give richer decomposition than sympy's flat `.args`. Missing: `.is_number` (check `tag == INT or tag == RAT`), `.has()` (walk tree or check `free_symbols`). |
87+
| **Structural inspection** | `isinstance(expr, sympy.X)`, `.is_number`, `.is_Atom`, `.args`, `.func` | `.tag` (tag-based dispatch), `.nchildren`, `.children`, `.child(i)`, `.has()`, specialized accessors | **None.** `isinstance` dispatch maps to `expr.tag == ixsimpl.ADD` etc. `.has()` is native. `.is_number` maps to `tag in (INT, RAT)`. Specialized accessors (`.add_nterms`, `.mul_nfactors`) give richer decomposition than sympy's flat `.args`. |
8588
| **Substitution** | `.subs()`, `.xreplace()`, `.replace(pred, fn)` | `.subs(target, repl)`, `.subs(mapping)` | **Soft.** Direct substitution is supported. Missing: `.replace(pred, fn)` (predicate-based bottom-up rewrite). Used in `_custom_simplify_once` for 4 transform passes. Would need a Python-level tree walker. |
86-
| **Free symbol inspection** | `.free_symbols`, `.has(sym)` | `.free_symbols` (Python `Expr` class) | **None.** Already implemented as a cached property on the Python `Expr` subclass. `.has()` can be trivially derived. |
89+
| **Free symbol inspection** | `.free_symbols`, `.has(sym)` | `.free_symbols` (cached property), `.has(sym)` | **None.** Both are native on the `Expr` class. |
8790
| **Expression decomposition** | `.as_ordered_terms()`, `.as_numer_denom()` | `.add_nterms`/`.add_term(i)`/`.add_term_coeff(i)`, `.rat_num`/`.rat_den` | **Soft.** Add decomposition is richer in ixsimpl (coefficient + terms). Numer/denom split is only used for Rational nodes (`.rat_num`/`.rat_den`). |
88-
| **Numeric probing** | `sympy.lambdify()` with custom modules | None | **Hard gap.** `lambdify` compiles sympy expressions to fast Python callables. Would need either an ixsimpl evaluator or a custom tree walker. `.to_c()` could potentially be used with ctypes but that is overkill. |
91+
| **Numeric probing** | `sympy.lambdify()` with custom modules | `ixsimpl.lambdify()`, `Expr.eval(env)` | **None.** `lambdify` is a near drop-in replacement (uses `.subs()` + constant folding). `Expr.eval(env)` handles single-point evaluation. No custom `modules` parameter needed -- ixsimpl handles floor/Mod/etc natively. |
8992
| **Affine conversion** | Sympy -> MLIR AffineExpr pipeline | None (but `.tag`-based walk is possible) | **Soft.** The converter does a structural walk over the expression tree. This maps directly to ixsimpl's `.tag` + `.child(i)` API. The walk structure would be nearly identical. |
90-
| **Constraint solving** | `sympy.solve()`, `sympy.Eq()` | `ctx.eq()` for construction, no solver | **Hard gap.** `sympy.solve()` used in 2 places (`assumptions.py`, `general_utils.py`). No ixsimpl equivalent. These are niche uses. |
91-
| **Codegen / printing** | `lambdastr()` for grid dim lambdas | `.to_c()` | **Soft.** `.to_c()` generates C code from expressions. `lambdastr()` generates Python code. Different targets, but the need is similar. Grid lambdas could use `.to_c()` + compile, or a Python eval walker. |
93+
| **Constraint solving** | `sympy.solve()`, `sympy.Eq()` | `ctx.eq()`, `ctx.check(expr, assumptions=)` | **None.** The sole `sympy.solve()` call is `evaluate_with_assumptions()` in `general_utils.py`, which checks whether an inequality is entailed/contradicted by a set of constraints -- returning `True`/`False`/`None`. This maps directly to `ctx.check(expr, assumptions=)`. |
94+
| **Codegen / printing** | `lambdastr()` for grid dim lambdas | `ixsimpl.lambdify()`, `.to_c()` | **None.** `ixsimpl.lambdify()` replaces the `lambdastr()` + `eval()` pattern directly. `.to_c()` available for C codegen if needed. |
9295
| **Type system** | `sympy.Integer`, `sympy.Rational`, `.is_Integer`, `.is_Rational` | `tag == INT`, `tag == RAT`, `.rat_num`, `.rat_den` | **None.** Tag-based dispatch replaces isinstance checks. |
9396

9497
### Summary of gaps
9598

9699
**Hard gaps** (no ixsimpl equivalent):
97-
1. `sympy.solve()` -- constraint solver (2 call sites, niche)
98-
2. `sympy.lambdify()` -- compiled expression evaluator (numeric probing)
99-
3. `.replace(pred, fn)` -- predicate-based rewriting (4 transforms in
100+
1. `.replace(pred, fn)` -- predicate-based bottom-up rewriting (4 transforms in
100101
`_custom_simplify_once`, but these exist only as fallback when ixsimpl
101-
conversion fails, so they may become dead code as coverage improves)
102+
conversion fails, so they may become dead code as coverage improves).
102103

103104
**Soft gaps** (ixsimpl has the feature, integration work needed):
104-
4. Symbol assumptions -- tracked separately, not on the symbol node
105-
5. `Abs` conversion -- emulate via `pw(x, x >= 0, -x, ctx.true_())`
106-
6. Affine converter -- structural walk needs porting from sympy isinstance to
107-
ixsimpl tag dispatch
108-
7. Grid lambda codegen -- `lambdastr` -> `.to_c()` or Python eval walker
105+
2. Symbol assumptions -- tracked separately, not on the symbol node.
106+
3. Affine converter -- structural walk needs porting from sympy isinstance to
107+
ixsimpl tag dispatch.
109108

110109
**No gap** (ready to use):
111-
8. Piecewise construction and inspection
112-
9. Free symbol inspection
113-
10. Substitution (direct mapping; predicate-based `.replace` excluded)
114-
11. Structural inspection via `.tag` and accessors
115-
12. Expression decomposition (add terms, mul factors, rational parts)
110+
5. Piecewise construction and inspection (`pw`, `.pw_*` accessors).
111+
6. Free symbol inspection (`.free_symbols`, `.has()`).
112+
7. Substitution (`.subs()`; predicate-based `.replace` excluded).
113+
8. Structural inspection (`.tag`, `.nchildren`, `.children`, `.child(i)`).
114+
9. Expression decomposition (`.add_*`, `.mul_*`, `.rat_*` accessors).
115+
10. Numeric probing and evaluation (`lambdify()`, `.eval()`).
116+
11. Absolute value (`abs_()`).
117+
12. Entailment queries (`ctx.check()` -- replaces `sympy.solve` in
118+
`evaluate_with_assumptions`).
119+
13. Codegen (`lambdify()`, `.to_c()`).
116120

117121
## Migration Strategy
118122

119-
ixsimpl is not just a simplifier -- it provides symbol creation, expression
120-
construction, structural inspection, substitution, free symbol queries,
121-
piecewise, expansion, and C codegen. The only true hard gaps vs sympy are
122-
`sympy.solve()` (2 call sites) and `lambdify()` (numeric probing).
123+
ixsimpl covers every sympy feature used in Wave: symbol creation, expression
124+
construction, structural inspection (`.tag` + accessors), substitution, free
125+
symbol queries, piecewise, evaluation (`lambdify`, `.eval()`), entailment
126+
checking (`ctx.check()`), expansion, and C codegen. The sole `sympy.solve()`
127+
call is an entailment check that maps directly to `ctx.check()`. The only
128+
remaining hard gap is `.replace(pred, fn)` (predicate-based rewriting), which
129+
lives in the sympy fallback path and may become dead code.
123130

124-
**Recommended end state: ixsimpl as the primary expression IR, sympy as a
125-
thin compatibility layer retained only for `solve()` and `lambdify()`.**
131+
**Recommended end state: ixsimpl as the sole expression IR. Sympy removed
132+
entirely as a runtime dependency.**
126133

127134
### Incremental phases
128135

@@ -296,25 +303,19 @@ caught by FileCheck.
296303

297304
## Phase 6: Remove sympy from hot paths
298305

299-
**Goal:** Compiler passes operate on ixsimpl `Expr` natively. Sympy is only
300-
used for:
301-
- `sympy.solve()` (2 call sites in `assumptions.py`, `general_utils.py`)
302-
- `sympy.lambdify()` (numeric probing in `symbol_utils.py`)
303-
- Legacy test assertions that compare sympy expressions
306+
**Goal:** Compiler passes operate on ixsimpl `Expr` natively. Sympy is removed
307+
as a runtime dependency.
304308

305309
**What this means:**
306310
- `IndexExpr` type alias changes from `sympy.Expr` to `ixsimpl.Expr`.
307311
- Symbol creation via `ctx.sym()` instead of `sympy.Symbol()`.
308312
- Expression construction via ixsimpl operators and free functions.
309313
- Substitution via `.subs()`.
314+
- Numeric probing via `ixsimpl.lambdify()` and `Expr.eval()`.
315+
- Entailment queries via `ctx.check()` (replaces `evaluate_with_assumptions`).
310316
- No more `piecewise_aware_subs()` workaround.
311317
- No more `evaluate=False` on Mod/floor (ixsimpl does not eagerly evaluate).
312-
313-
**The 2 remaining sympy uses:**
314-
- `sympy.solve()`: Convert ixsimpl -> sympy at the call site, solve, convert
315-
back. This is 2 call sites total.
316-
- `lambdify()`: Either keep the sympy conversion for probing, or write a simple
317-
Python evaluator that walks ixsimpl tags. The latter is ~50 lines.
318+
- No more sympy bug workarounds (#28744, floor/ceil evaluation bugs).
318319

319320
**Risk:** High but contained. This is the "flip the switch" phase. Every
320321
preceding phase must be complete and validated.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ dependencies = [
3737
"sympy",
3838
"torch>=2.6,<2.10",
3939
"typing_extensions",
40-
"ixsimpl @ git+https://github.com/hardcode84/4.git@b67ca06a522a769aca8553fbea549bb2bd9d4afa",
40+
"ixsimpl @ git+https://github.com/hardcode84/4.git@c72cde172ce15ba2bd5ede7d6f2037a6e391fa3b",
4141
]
4242

4343
dynamic = ["version"]

0 commit comments

Comments
 (0)