Skip to content

Commit b34c98e

Browse files
committed
fold conj, real, imag on the symbolic imaginary unit
Add three `@match` arms in `Base.real`, `Base.conj`, `Base.imag` for `BasicSymbolic` that match `Sym(:im; type = Number)` structurally and fold to `0`, `-im`, and `1` respectively. `Symbolics.IM` is defined as a `Sym{VartypeT}(:im; type = Number)` and used as a stand-in for `1im` to keep expressions inside `BasicSymbolic{<:Real}` algebra (where multiplying by a Julia `Complex` literal would otherwise materialise an opaque `complex(re, im)` `Term`). Until now those three operations on `IM` produced opaque `conj(im)` / `real(im)` / `imag(im)` wrappers that `simplify` could not reduce, so downstream code that algebraically conjugates `IM`-bearing expressions (e.g. SQA's `qadjoint` / `inner_adjoint`) had to special-case `IM` themselves. Matching is structural (name + symtype) rather than by identity, so no new dependency on Symbolics is introduced. A same-named sym with a non-`Number` symtype is left alone (test case).
1 parent 7ffce39 commit b34c98e

2 files changed

Lines changed: 22 additions & 0 deletions

File tree

src/methods.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,9 @@ function Base.real(s::BasicSymbolic{T}) where {T}
479479
@match s begin
480480
BSImpl.Const(; val) => Const{T}(real(val))
481481
BSImpl.Term(; f, args) && if f === complex && length(args) == 2 end => args[1]
482+
# The symbolic imaginary unit (`Symbolics.IM`) is `Sym{T}(:im; type = Number)`.
483+
# `real(im) == 0`. Matching structurally avoids a Symbolics-side dependency.
484+
BSImpl.Sym(; name) && if name === :im && symtype(s) === Number end => zero_of_vartype(T)
482485
_ => Term{T}(real, ArgsT{T}((s,)); type = Real)
483486
end
484487
end
@@ -490,6 +493,10 @@ function Base.conj(s::BasicSymbolic{T}) where {T}
490493
BSImpl.Term(; f, args, type, shape) && if f === complex && length(args) == 2 end => begin
491494
BSImpl.Term{T}(f, ArgsT{T}(args[1], -args[2]); type, shape)
492495
end
496+
# `conj(im) = -im`. Lets `simplify` and the downstream `qadjoint` /
497+
# `inner_adjoint` helpers (in SecondQuantizedAlgebra.jl) fold cleanly
498+
# through expressions that use `Symbolics.IM` to avoid `Complex{Num}`.
499+
BSImpl.Sym(; name) && if name === :im && symtype(s) === Number end => -s
493500
_ => Term{T}(conj, ArgsT{T}((s,)); type = symtype(s), shape = shape(s))
494501
end
495502
end
@@ -498,6 +505,8 @@ function Base.imag(s::BasicSymbolic{T}) where {T}
498505
@match s begin
499506
BSImpl.Const(; val) => Const{T}(imag(val))
500507
BSImpl.Term(; f, args) && if f === complex && length(args) == 2 end => args[2]
508+
# `imag(im) == 1`.
509+
BSImpl.Sym(; name) && if name === :im && symtype(s) === Number end => one_of_vartype(T)
501510
_ => Term{T}(imag, ArgsT{T}((s,)); type = Real)
502511
end
503512
end

test/rulesets.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ end
5959
@test unwrap_const(simplify(Term{SymReal}(zero, [a]))) == 0
6060
@test unwrap_const(simplify(Term{SymReal}(zero, [b + 1]))) == 0
6161
@test unwrap_const(simplify(Term{SymReal}(zero, [x + 2]))) == 0
62+
63+
# Symbolic imaginary unit (matches `Symbolics.IM = Sym(:im; type = Number)`).
64+
# Without these fold rules, `conj(im)`/`real(im)`/`imag(im)` stay wrapped
65+
# and downstream simplification can never reduce expressions that use
66+
# `IM` to avoid `Complex{Num}` materialisation.
67+
let IM = SymbolicUtils.Sym{SymReal}(:im; type = Number)
68+
@eqtest conj(IM) == -IM
69+
@test unwrap_const(real(IM)) == 0
70+
@test unwrap_const(imag(IM)) == 1
71+
# A same-named sym with a non-Number symtype is unaffected.
72+
not_im = SymbolicUtils.Sym{SymReal}(:im; type = Real)
73+
@eqtest conj(not_im) == not_im
74+
end
6275
end
6376

6477
@testset "LiteralReal" begin

0 commit comments

Comments
 (0)