From b1cee9eadde49705320710633d94d9920af3a14d Mon Sep 17 00:00:00 2001 From: Blanca Luo Date: Mon, 11 Nov 2024 13:10:21 -0500 Subject: [PATCH 01/13] Apply hash consing to `Term` --- src/types.jl | 7 +++++-- test/hash_consing.jl | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/types.jl b/src/types.jl index 8a8c7361..81220ebd 100644 --- a/src/types.jl +++ b/src/types.jl @@ -97,6 +97,7 @@ function ConstructionBase.setproperties(obj::BasicSymbolic{T}, patch::NamedTuple # Call outer constructor because hash consing cannot be applied in inner constructor @compactified obj::BasicSymbolic begin Sym => Sym{T}(nt_new.name; nt_new...) + Term => Term{T}(nt_new.f, nt_new.arguments; nt_new...) _ => Unityper.rt_constructor(obj){T}(;nt_new...) end end @@ -395,11 +396,13 @@ function Term{T}(f, args; kw...) where T args = convert(Vector{Any}, args) end - Term{T}(;f=f, arguments=args, hash=Ref(UInt(0)), kw...) + s = Term{T}(;f=f, arguments=args, hash=Ref(UInt(0)), kw...) + BasicSymbolic(s) end function Term(f, args; metadata=NO_METADATA) - Term{_promote_symtype(f, args)}(f, args, metadata=metadata) + s = Term{_promote_symtype(f, args)}(f, args, metadata=metadata) + BasicSymbolic(s) end function Add(::Type{T}, coeff, dict; metadata=NO_METADATA, kw...) where T diff --git a/test/hash_consing.jl b/test/hash_consing.jl index aaf97997..82385736 100644 --- a/test/hash_consing.jl +++ b/test/hash_consing.jl @@ -1,4 +1,5 @@ using SymbolicUtils, Test +using SymbolicUtils: Term struct Ctx1 end struct Ctx2 end @@ -24,3 +25,18 @@ struct Ctx2 end xm3 = setmetadata(x1, Ctx2, "meta_2") @test xm1 !== xm3 end + +@syms a b c + +@testset "Term" begin + t1 = sin(a) + t2 = sin(a) + @test t1 === t2 + t3 = Term(identity,[a]) + t4 = Term(identity,[a]) + @test t3 === t4 + t5 = Term{Int}(identity,[a]) + @test t3 !== t5 + tm1 = setmetadata(t1, Ctx1, "meta_1") + @test t1 !== tm1 +end From 25d37f12efdc6f3a4143edeea5a039d265c1fd0e Mon Sep 17 00:00:00 2001 From: Blanca Luo Date: Mon, 11 Nov 2024 15:22:23 -0500 Subject: [PATCH 02/13] Apply hash consing to `Add` --- src/types.jl | 4 +++- test/hash_consing.jl | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/types.jl b/src/types.jl index 81220ebd..f2b0cfa9 100644 --- a/src/types.jl +++ b/src/types.jl @@ -98,6 +98,7 @@ function ConstructionBase.setproperties(obj::BasicSymbolic{T}, patch::NamedTuple @compactified obj::BasicSymbolic begin Sym => Sym{T}(nt_new.name; nt_new...) Term => Term{T}(nt_new.f, nt_new.arguments; nt_new...) + Add => Add(T, nt_new.coeff, nt_new.dict; nt_new...) _ => Unityper.rt_constructor(obj){T}(;nt_new...) end end @@ -418,7 +419,8 @@ function Add(::Type{T}, coeff, dict; metadata=NO_METADATA, kw...) where T end end - Add{T}(; coeff, dict, hash=Ref(UInt(0)), metadata, arguments=[], issorted=RefValue(false), kw...) + s = Add{T}(; coeff, dict, hash=Ref(UInt(0)), metadata, arguments=[], issorted=RefValue(false), kw...) + BasicSymbolic(s) end function Mul(T, a, b; metadata=NO_METADATA, kw...) diff --git a/test/hash_consing.jl b/test/hash_consing.jl index 82385736..d739dbbc 100644 --- a/test/hash_consing.jl +++ b/test/hash_consing.jl @@ -1,5 +1,5 @@ using SymbolicUtils, Test -using SymbolicUtils: Term +using SymbolicUtils: Term, Add struct Ctx1 end struct Ctx2 end @@ -40,3 +40,17 @@ end tm1 = setmetadata(t1, Ctx1, "meta_1") @test t1 !== tm1 end + +@testset "Add" begin + d1 = a + b + d2 = b + a + @test d1 === d2 + d3 = b - 2 + a + d4 = a + b - 2 + @test d3 === d4 + d5 = Add(Int, 0, Dict(a => 1, b => 1)) + @test d5 !== d1 + + dm1 = setmetadata(d1,Ctx1,"meta_1") + @test d1 !== dm1 +end From e44a4571ede1e55bea27fea3511c50e5fede279a Mon Sep 17 00:00:00 2001 From: Blanca Luo Date: Tue, 12 Nov 2024 08:44:53 -0500 Subject: [PATCH 03/13] Apply hash consing to `Mul` --- src/types.jl | 4 +++- test/hash_consing.jl | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/types.jl b/src/types.jl index f2b0cfa9..96c137d0 100644 --- a/src/types.jl +++ b/src/types.jl @@ -99,6 +99,7 @@ function ConstructionBase.setproperties(obj::BasicSymbolic{T}, patch::NamedTuple Sym => Sym{T}(nt_new.name; nt_new...) Term => Term{T}(nt_new.f, nt_new.arguments; nt_new...) Add => Add(T, nt_new.coeff, nt_new.dict; nt_new...) + Mul => Mul(T, nt_new.coeff, nt_new.dict; nt_new...) _ => Unityper.rt_constructor(obj){T}(;nt_new...) end end @@ -435,7 +436,8 @@ function Mul(T, a, b; metadata=NO_METADATA, kw...) else coeff = a dict = b - Mul{T}(; coeff, dict, hash=Ref(UInt(0)), metadata, arguments=[], issorted=RefValue(false), kw...) + s = Mul{T}(; coeff, dict, hash=Ref(UInt(0)), metadata, arguments=[], issorted=RefValue(false), kw...) + BasicSymbolic(s) end end diff --git a/test/hash_consing.jl b/test/hash_consing.jl index d739dbbc..548d3bf6 100644 --- a/test/hash_consing.jl +++ b/test/hash_consing.jl @@ -1,5 +1,5 @@ using SymbolicUtils, Test -using SymbolicUtils: Term, Add +using SymbolicUtils: Term, Add, Mul struct Ctx1 end struct Ctx2 end @@ -54,3 +54,17 @@ end dm1 = setmetadata(d1,Ctx1,"meta_1") @test d1 !== dm1 end + +@testset "Mul" begin + m1 = a*b + m2 = b*a + @test m1 === m2 + m3 = 6*a*b + m4 = 3*a*2*b + @test m3 === m4 + m5 = Mul(Int, 1, Dict(a => 1, b => 1)) + @test m5 !== m1 + + mm1 = setmetadata(m1, Ctx1, "meta_1") + @test m1 !== mm1 +end From 3dc116930a0685e4c8da107df2ce8fd10523d6b3 Mon Sep 17 00:00:00 2001 From: Blanca Luo Date: Wed, 13 Nov 2024 09:47:06 -0500 Subject: [PATCH 04/13] Apply hash consing to `Div` --- src/types.jl | 7 +++++-- test/hash_consing.jl | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/types.jl b/src/types.jl index 96c137d0..24b30dce 100644 --- a/src/types.jl +++ b/src/types.jl @@ -100,6 +100,7 @@ function ConstructionBase.setproperties(obj::BasicSymbolic{T}, patch::NamedTuple Term => Term{T}(nt_new.f, nt_new.arguments; nt_new...) Add => Add(T, nt_new.coeff, nt_new.dict; nt_new...) Mul => Mul(T, nt_new.coeff, nt_new.dict; nt_new...) + Div => Div{T}(nt_new.num, nt_new.den, nt_new.simplified; nt_new...) _ => Unityper.rt_constructor(obj){T}(;nt_new...) end end @@ -502,11 +503,13 @@ function Div{T}(n, d, simplified=false; metadata=nothing) where {T} end end - Div{T}(; num=n, den=d, simplified, arguments=[], metadata) + s = Div{T}(; num=n, den=d, simplified, arguments=[], metadata) + BasicSymbolic(s) end function Div(n,d, simplified=false; kw...) - Div{promote_symtype((/), symtype(n), symtype(d))}(n, d, simplified; kw...) + s = Div{promote_symtype((/), symtype(n), symtype(d))}(n, d, simplified; kw...) + BasicSymbolic(s) end @inline function numerators(x) diff --git a/test/hash_consing.jl b/test/hash_consing.jl index 548d3bf6..b9725fdc 100644 --- a/test/hash_consing.jl +++ b/test/hash_consing.jl @@ -1,5 +1,5 @@ using SymbolicUtils, Test -using SymbolicUtils: Term, Add, Mul +using SymbolicUtils: Term, Add, Mul, Div struct Ctx1 end struct Ctx2 end @@ -68,3 +68,20 @@ end mm1 = setmetadata(m1, Ctx1, "meta_1") @test m1 !== mm1 end + +@testset "Div" begin + v1 = a/b + v2 = a/b + @test v1 === v2 + v3 = -1/a + v4 = -1/a + @test v3 === v4 + v5 = 3a/6 + v6 = 2a/4 + @test v5 === v6 + v7 = Div{Float64}(-1,a) + @test v7 !== v3 + + vm1 = setmetadata(v1,Ctx1, "meta_1") + @test vm1 !== v1 +end From c8b2f665a3de4a182202795a49ca69b4231c8b93 Mon Sep 17 00:00:00 2001 From: "Bowen S. Zhu" Date: Thu, 14 Nov 2024 12:52:17 -0500 Subject: [PATCH 05/13] Avoid applying hash consing twice --- src/types.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/types.jl b/src/types.jl index 24b30dce..539814c8 100644 --- a/src/types.jl +++ b/src/types.jl @@ -404,8 +404,7 @@ function Term{T}(f, args; kw...) where T end function Term(f, args; metadata=NO_METADATA) - s = Term{_promote_symtype(f, args)}(f, args, metadata=metadata) - BasicSymbolic(s) + Term{_promote_symtype(f, args)}(f, args, metadata=metadata) end function Add(::Type{T}, coeff, dict; metadata=NO_METADATA, kw...) where T @@ -508,8 +507,7 @@ function Div{T}(n, d, simplified=false; metadata=nothing) where {T} end function Div(n,d, simplified=false; kw...) - s = Div{promote_symtype((/), symtype(n), symtype(d))}(n, d, simplified; kw...) - BasicSymbolic(s) + Div{promote_symtype((/), symtype(n), symtype(d))}(n, d, simplified; kw...) end @inline function numerators(x) From 710f871b960e972a564b60eacf7d77cd3ad0709d Mon Sep 17 00:00:00 2001 From: Blanca Luo Date: Thu, 14 Nov 2024 14:30:23 -0500 Subject: [PATCH 06/13] Apply hash consing to `Pow` --- src/types.jl | 4 +++- test/hash_consing.jl | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/types.jl b/src/types.jl index 58ef30ae..65f2c793 100644 --- a/src/types.jl +++ b/src/types.jl @@ -101,6 +101,7 @@ function ConstructionBase.setproperties(obj::BasicSymbolic{T}, patch::NamedTuple Add => Add(T, nt_new.coeff, nt_new.dict; nt_new...) Mul => Mul(T, nt_new.coeff, nt_new.dict; nt_new...) Div => Div{T}(nt_new.num, nt_new.den, nt_new.simplified; nt_new...) + Pow => Pow{T}(nt_new.base, nt_new.exp; nt_new...) _ => Unityper.rt_constructor(obj){T}(;nt_new...) end end @@ -520,7 +521,8 @@ end function Pow{T}(a, b; metadata=NO_METADATA) where {T} _iszero(b) && return 1 _isone(b) && return a - Pow{T}(; base=a, exp=b, arguments=[], metadata) + s = Pow{T}(; base=a, exp=b, arguments=[], metadata) + BasicSymbolic(s) end function Pow(a, b; metadata=NO_METADATA) diff --git a/test/hash_consing.jl b/test/hash_consing.jl index b9725fdc..c04b6152 100644 --- a/test/hash_consing.jl +++ b/test/hash_consing.jl @@ -1,5 +1,5 @@ using SymbolicUtils, Test -using SymbolicUtils: Term, Add, Mul, Div +using SymbolicUtils: Term, Add, Mul, Div, Pow struct Ctx1 end struct Ctx2 end @@ -85,3 +85,17 @@ end vm1 = setmetadata(v1,Ctx1, "meta_1") @test vm1 !== v1 end + +@testset "Pow" begin + p1 = a^b + p2 = a^b + @test p1 === p2 + p3 = a^(2^-b) + p4 = a^(2^-b) + @test p3 === p4 + p5 = Pow{Float64}(a,b) + @test p1 !== p5 + + pm1 = setmetadata(p1,Ctx1, "meta_1") + @test pm1 !== p1 +end From 3b264aaa58ad4f40183f8985e52fadc120b38a04 Mon Sep 17 00:00:00 2001 From: "Bowen S. Zhu" Date: Thu, 14 Nov 2024 14:38:30 -0500 Subject: [PATCH 07/13] feat: Add support for extra keyword arguments to `Div` and `Pow` --- src/types.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types.jl b/src/types.jl index 65f2c793..8f8afbde 100644 --- a/src/types.jl +++ b/src/types.jl @@ -469,7 +469,7 @@ function maybe_intcoeff(x) end end -function Div{T}(n, d, simplified=false; metadata=nothing) where {T} +function Div{T}(n, d, simplified=false; metadata=nothing, kwargs...) where {T} if T<:Number && !(T<:SafeReal) n, d = quick_cancel(n, d) end @@ -518,15 +518,15 @@ end @inline denominators(x) = isdiv(x) ? numerators(x.den) : Any[1] -function Pow{T}(a, b; metadata=NO_METADATA) where {T} +function Pow{T}(a, b; metadata=NO_METADATA, kwargs...) where {T} _iszero(b) && return 1 _isone(b) && return a s = Pow{T}(; base=a, exp=b, arguments=[], metadata) BasicSymbolic(s) end -function Pow(a, b; metadata=NO_METADATA) - Pow{promote_symtype(^, symtype(a), symtype(b))}(makepow(a, b)..., metadata=metadata) +function Pow(a, b; metadata = NO_METADATA, kwargs...) + Pow{promote_symtype(^, symtype(a), symtype(b))}(makepow(a, b)...; metadata, kwargs...) end function toterm(t::BasicSymbolic{T}) where T From f2d779194cd3d4863f366455e82ecebee358446e Mon Sep 17 00:00:00 2001 From: "Bowen S. Zhu" Date: Mon, 25 Nov 2024 14:02:29 -0500 Subject: [PATCH 08/13] Make `hash2` recursively incorporate metadata and symtype --- src/types.jl | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/types.jl b/src/types.jl index 8f8afbde..0e1eb123 100644 --- a/src/types.jl +++ b/src/types.jl @@ -303,6 +303,7 @@ Base.nameof(s::BasicSymbolic) = issym(s) ? s.name : error("None Sym BasicSymboli ## This is much faster than hash of an array of Any hashvec(xs, z) = foldr(hash, xs, init=z) +hashvec2(xs, z) = foldr(hash2, xs, init=z) const SYM_SALT = 0x4de7d7c66d41da43 % UInt const ADD_SALT = 0xaddaddaddaddadda % UInt const SUB_SALT = 0xaaaaaaaaaaaaaaaa % UInt @@ -350,9 +351,33 @@ includes the metadata and symtype in the hash calculation. This can be beneficia consing, allowing for more effective deduplication of symbolically equivalent expressions with different metadata or symtypes. """ +hash2(s, salt::UInt) = hash(s, salt) hash2(s::BasicSymbolic) = hash2(s, zero(UInt)) function hash2(s::BasicSymbolic{T}, salt::UInt)::UInt where {T} - hash(metadata(s), hash(T, hash(s, salt))) + E = exprtype(s) + h::UInt = 0 + if E === SYM + h = hash(nameof(s), salt ⊻ SYM_SALT) + elseif E === ADD || E === MUL + hashoffset = isadd(s) ? ADD_SALT : SUB_SALT + hv = Base.hasha_seed + for (k, v) in s.dict + hv ⊻= hash2(k, hash(v)) + end + h = hash(hv, salt) + h = hash(hashoffset, hash(s.coeff, h)) + elseif E === DIV + h = hash2(s.num, hash2(s.den, salt ⊻ DIV_SALT)) + elseif E === POW + h = hash2(s.exp, hash2(s.base, salt ⊻ POW_SALT)) + elseif E === TERM + op = operation(s) + oph = op isa Function ? nameof(op) : op + h = hashvec2(arguments(s), hash(oph, salt)) + else + error_on_type() + end + hash(metadata(s), hash(T, h)) end ### From 680d148e6bd3229eba22e36ad753cbe2491241cd Mon Sep 17 00:00:00 2001 From: "Bowen S. Zhu" Date: Mon, 25 Nov 2024 15:20:27 -0500 Subject: [PATCH 09/13] Hash equivalent numbers with different types differently --- src/types.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types.jl b/src/types.jl index 0e1eb123..b10920e7 100644 --- a/src/types.jl +++ b/src/types.jl @@ -352,6 +352,9 @@ consing, allowing for more effective deduplication of symbolically equivalent ex with different metadata or symtypes. """ hash2(s, salt::UInt) = hash(s, salt) +function hash2(n::T, salt::UInt) where {T <: Number} + hash(T, hash(n, salt)) +end hash2(s::BasicSymbolic) = hash2(s, zero(UInt)) function hash2(s::BasicSymbolic{T}, salt::UInt)::UInt where {T} E = exprtype(s) From e5fb7ce68b25bf7879b9911b99b3b72b1081c3f3 Mon Sep 17 00:00:00 2001 From: "Bowen S. Zhu" Date: Wed, 27 Nov 2024 01:04:14 -0500 Subject: [PATCH 10/13] Differentiate equivalent `coeff` with different numeric types --- src/types.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.jl b/src/types.jl index b10920e7..c48ca14b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -368,7 +368,7 @@ function hash2(s::BasicSymbolic{T}, salt::UInt)::UInt where {T} hv ⊻= hash2(k, hash(v)) end h = hash(hv, salt) - h = hash(hashoffset, hash(s.coeff, h)) + h = hash(hashoffset, hash2(s.coeff, h)) elseif E === DIV h = hash2(s.num, hash2(s.den, salt ⊻ DIV_SALT)) elseif E === POW From 3f3d013226e21c3752d7ec900d757beacbe29f3f Mon Sep 17 00:00:00 2001 From: "Bowen S. Zhu" Date: Wed, 27 Nov 2024 18:08:26 -0500 Subject: [PATCH 11/13] Add test for `hash2` for equivalent numbers --- test/hash_consing.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/hash_consing.jl b/test/hash_consing.jl index c04b6152..92a9d6d8 100644 --- a/test/hash_consing.jl +++ b/test/hash_consing.jl @@ -1,5 +1,5 @@ using SymbolicUtils, Test -using SymbolicUtils: Term, Add, Mul, Div, Pow +using SymbolicUtils: Term, Add, Mul, Div, Pow, hash2 struct Ctx1 end struct Ctx2 end @@ -99,3 +99,10 @@ end pm1 = setmetadata(p1,Ctx1, "meta_1") @test pm1 !== p1 end + +@testset "Equivalent numbers" begin + f = 0.5 + r = 1 // 2 + @test hash(f) == hash(r) + @test hash2(f) != hash2(r) +end From 1672b2ff7b1c07df797a61a87a48d2313df2fe03 Mon Sep 17 00:00:00 2001 From: "Bowen S. Zhu" Date: Wed, 27 Nov 2024 18:12:31 -0500 Subject: [PATCH 12/13] Add docstring in `hash2` about equivalent numbers --- src/types.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/types.jl b/src/types.jl index c48ca14b..2572bd8b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -350,6 +350,12 @@ objects. Unlike `Base.hash`, which only considers the expression structure, `has includes the metadata and symtype in the hash calculation. This can be beneficial for hash consing, allowing for more effective deduplication of symbolically equivalent expressions with different metadata or symtypes. + +Equivalent numbers of different types, such as `0.5::Float64` and +`(1 // 2)::Rational{Int64}`, have the same default `Base.hash` value. The `hash2` function +distinguishes these by including their numeric types in the hash calculation to ensure that +symbolically equivalent expressions with different numeric types are treated as distinct +objects. """ hash2(s, salt::UInt) = hash(s, salt) function hash2(n::T, salt::UInt) where {T <: Number} From d716d3b09ea1154d8199d22686d186bdf2e03b99 Mon Sep 17 00:00:00 2001 From: "Bowen S. Zhu" Date: Wed, 27 Nov 2024 18:25:30 -0500 Subject: [PATCH 13/13] Add test for hash of symbolic expressions involving equivalent numbers --- test/hash_consing.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/hash_consing.jl b/test/hash_consing.jl index 92a9d6d8..aa6aa7a9 100644 --- a/test/hash_consing.jl +++ b/test/hash_consing.jl @@ -104,5 +104,7 @@ end f = 0.5 r = 1 // 2 @test hash(f) == hash(r) - @test hash2(f) != hash2(r) + u0 = zero(UInt) + @test hash2(f, u0) != hash2(r, u0) + @test f + a !== r + a end