diff --git a/Changelog.md b/Changelog.md index 3f841633..134b56bd 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,12 @@ All notable changes to [`SecondQuantizedAlgebra.jl`](https://github.com/qojulia/ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.6.4] + +### Fixed + +- `change_index` now correctly zeros `DoubleIndexedVariable` nodes tagged `identical=false` even when they appear nested inside a larger product or sum. Previously `_check_not_identical` only examined the root of the substituted expression, so a `J(i,j)` factor inside `J(i,j) * x` would survive a `j → i` substitution intact instead of collapsing to zero. The fix walks the full expression tree, collects every `NotIdentical`-tagged node whose two index arguments became equal, and replaces them all via a single `Symbolics.substitute` pass. + ## [v0.6.3] ### Changed @@ -160,4 +166,5 @@ These names keep their meaning across the migration. Code that only uses them sh [v0.6.1]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.6.1 [v0.6.2]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.6.2 [v0.6.3]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.6.3 +[v0.6.4]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/releases/tag/v0.6.4 [#156]: https://github.com/qojulia/SecondQuantizedAlgebra.jl/issues/156 diff --git a/Project.toml b/Project.toml index f4130b0a..3bf5232f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "SecondQuantizedAlgebra" uuid = "f7aa4685-e143-4cb6-a7f3-073579757907" -version = "0.6.3" +version = "0.6.4" [deps] Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" diff --git a/src/expressions/index.jl b/src/expressions/index.jl index d233073c..fb312cfd 100644 --- a/src/expressions/index.jl +++ b/src/expressions/index.jl @@ -256,18 +256,34 @@ function create_index_arrays(indices::Vector{Index}, ranges::Vector{<:AbstractRa return vec(collect(Iterators.product(ranges...))) end -"""Check if a substituted BasicSymbolic node with NotIdentical metadata has equal args → 0.""" +""" +Zero every `NotIdentical`-tagged node anywhere in a substituted expression whose +two indices became equal. Nested occurrences matter: a `DoubleIndexedVariable` +with `identical=false` is almost always a factor in a larger product or sum, and +the surrounding `*`/`+` only collapses once that factor is replaced by zero. +""" function _check_not_identical(result::SymbolicUtils.BasicSymbolic) - if SymbolicUtils.iscall(result) && - SymbolicUtils.hasmetadata(result, NotIdentical) && - length(SymbolicUtils.arguments(result)) == 2 - a1, a2 = SymbolicUtils.arguments(result) - isequal(a1, a2) && return Num(0) - end - return Num(result) + zeros = _collect_identical_zeros!(Dict{SymbolicUtils.BasicSymbolic, Int}(), result) + isempty(zeros) && return Num(result) + return Num(Symbolics.substitute(result, zeros)) end _check_not_identical(result::Number) = Num(result) +"""Accumulate into `acc` every `NotIdentical` node in `x` with two equal args, mapped to 0.""" +function _collect_identical_zeros!(acc, x) + (x isa SymbolicUtils.BasicSymbolic && SymbolicUtils.iscall(x)) || return acc + if SymbolicUtils.hasmetadata(x, NotIdentical) && + length(SymbolicUtils.arguments(x)) == 2 + a1, a2 = SymbolicUtils.arguments(x) + # The args are indices, so a matched node holds no nested NotIdentical node. + isequal(a1, a2) && (acc[x] = 0; return acc) + end + for a in SymbolicUtils.arguments(x) + _collect_identical_zeros!(acc, a) + end + return acc +end + function _depends_on_index_term(c::CNum, ops::Vector{QSym}, idx::Index) for op in ops op.index == idx && return true diff --git a/test/expressions/indexing_test.jl b/test/expressions/indexing_test.jl index fac0fd7c..e2374eea 100644 --- a/test/expressions/indexing_test.jl +++ b/test/expressions/indexing_test.jl @@ -378,6 +378,20 @@ import SecondQuantizedAlgebra: simplify, QAdd, QSym, CNum, _to_cnum, NO_INDEX, @test isequal(real(prefactor(mj)), gj) end + @testset "change_index: nested identical=false collapses to 0" begin + i = Index(hf, :i, 10, hf) + j = Index(hf, :j, 10, hf) + @variables x + Jij = DoubleIndexedVariable(:J, i, j; identical = false) # 0 when i==j + + # Bare node already worked; the regression is nested occurrences. + @test isequal(change_index(Jij, j, i), Symbolics.Num(0)) + # Under a product: J(i,j)*x with j→i ⇒ J(i,i)=0 ⇒ whole product 0 + @test isequal(change_index(Jij * x, j, i), Symbolics.Num(0)) + # Under a sum: (J(i,j) + x) with j→i ⇒ x + @test isequal(change_index(Jij + x, j, i), x) + end + @testset "change_index — QAdd" begin i = Index(hf, :i, 10, hf) j = Index(hf, :j, 10, hf)