From b272324b9de2ebbc3f859c4bc30425eb74620089 Mon Sep 17 00:00:00 2001 From: Orjan Ameye Date: Wed, 24 Jun 2026 14:43:34 +0200 Subject: [PATCH] fix(translate): drop identically-zero terms in time-dependent translation The time-dependent path of `translate_qo` kept terms whose coefficient was identically zero after parameter substitution. A coupling substituted to zero leaves an unfolded `conj(0.0)` factor (or `conj(conj(0.0))` when a cascade Hamiltonian's second cross-term is formed as `adjoint(X)`), which Symbolics does not fold to zero, so the term survived and the compiled closure re-evaluated it (pulse interpolation plus operator rebuild) on every ODE step. Fold `conj` of a numeric literal in each coefficient, then drop the coefficients that become zero. `conj` of a symbolic time variable is left untouched, so genuinely time-dependent complex couplings are unaffected. Effect on the correlation benchmark (single-photon cavity, g_v set to 0): allocations fall from 5.17M to 2.78M and memory from 555 MB to 373 MB, both back to the SecondQuantizedAlgebra 0.4.5 level; per-call H(t) allocation goes from 6128 B to 2080 B. Output is bit-identical (dropping identically-zero terms is exact), numeric SLH composition is unaffected (this does not touch the cascade), and translate construction time is unchanged. test_translate and test_correlations pass. --- src/translate.jl | 51 ++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/translate.jl b/src/translate.jl index 12c1de4..f413481 100644 --- a/src/translate.jl +++ b/src/translate.jl @@ -182,6 +182,21 @@ function _compile_coeff(c::Complex{Num}, vars...) return (vals...) -> g_re(vals...) + im * g_im(vals...) end +# Fold `conj` applied to a numeric literal. After substituting a parameter to a +# number, a coefficient can carry `conj(0.0)` (a real coupling set to zero) or +# `conj(conj(0.0))` (from a cascade Hamiltonian's adjoint cross-term). Symbolics +# does not fold these, so the term still looks nonzero and the time-dependent +# closure re-evaluates it on every ODE step. Folding `conj`-of-a-number lets such +# coefficients collapse to 0 so the term can be dropped. `conj` of a *symbolic* +# (time) variable is left untouched. +const _FOLD_NUM_CONJ = SymbolicUtils.Postwalk( + SymbolicUtils.PassThrough(SymbolicUtils.@rule conj(~x::(y -> y isa Number)) => conj(~x)), +) +_fold_num_conj(c::Complex{Num}) = Complex{Num}( + Num(_FOLD_NUM_CONJ(SymbolicUtils.unwrap(real(c)))), + Num(_FOLD_NUM_CONJ(SymbolicUtils.unwrap(imag(c)))), +) + # ── Per-term translation ── # Translate a single `(ops, coefficient)` term into a time-dependent function # `t -> op`. A concrete coefficient yields a constant function; a symbolic @@ -250,29 +265,27 @@ function _translate_qo( op_ = substitute(op, parameter) iszero(op_) && return t -> op_type(0 * one(b)) - pairs = collect(op_.arguments) + # Convert each coefficient to a foldable numeric form and drop terms that are + # identically zero after substitution. Without this, a coupling set to zero + # leaves an unfolded conj(0)/conj(conj(0)) factor, so the term survives and is + # needlessly rebuilt every ODE step (see `_fold_num_conj`). + pairs = Tuple{Any,Complex{Num}}[] + for (term, c_) in op_.arguments + c = _fold_num_conj(_coeff_num(c_)) + iszero(c) && continue + push!(pairs, (term, c)) + end + isempty(pairs) && return t -> op_type(0 * one(b)) + if length(pairs) == 1 (term, c) = pairs[1] - return _translate_term( - term.ops, - _coeff_num(c), - b, - time_parameter, - operators, - op_type, - ) + return _translate_term(term.ops, c, b, time_parameter, operators, op_type) end # Multi-term: combine via FunctionWrappers with a common concrete type so the # returned closure is type-stable. Inner products use `sparse` for a uniform type. - first_res = _translate_term( - pairs[1].first.ops, - _coeff_num(pairs[1].second), - b, - time_parameter, - operators, - sparse, - ) + first_res = + _translate_term(pairs[1][1].ops, pairs[1][2], b, time_parameter, operators, sparse) OpType = typeof(first_res(0.0)) FW = FunctionWrapper{OpType,Tuple{Float64}} @@ -280,8 +293,8 @@ function _translate_qo( res = k == 1 ? first_res : _translate_term( - pairs[k].first.ops, - _coeff_num(pairs[k].second), + pairs[k][1].ops, + pairs[k][2], b, time_parameter, operators,