Skip to content

Commit 8b8bc98

Browse files
fix: properly eliminate identically zero variables in alias_elimination!
1 parent 15904e3 commit 8b8bc98

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

src/systems/alias_elimination.jl

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,107 @@ function find_perfect_aliases!(
409409
return aliases
410410
end
411411

412+
"""
413+
$TYPEDSIGNATURES
414+
415+
If a row in `mm` only contains a single variable, that variable is identically zero.
416+
Eliminate such any such zero variables which can be identified from `mm`. Return:
417+
- `zero_vars`: A `Set{Int}` of variables identified to be identically zero.
418+
- `zero_eqs`: A `Set{Int}` of equations identifying such variables to be zero.
419+
- `eqs_to_substitute`: A `Set{Int}` of equations in `state` which contain variables
420+
in `zero_vars`. These equations should be substituted to remove the zero variables.
421+
422+
Also update `mm` in-place to remove such zero variables/equations.
423+
"""
424+
function eliminate_zero_variables!(state::TearingState, mm::CLIL.SparseMatrixCLIL)
425+
# Inverse mapping of `mm.nzrows`
426+
nzrow_to_idx = Dict{Int, Int}()
427+
sizehint!(nzrow_to_idx, length(mm.nzrows))
428+
for (i, eqidx) in enumerate(mm.nzrows)
429+
nzrow_to_idx[eqidx] = i
430+
end
431+
# Variables we can identify are zero via `mm`
432+
zero_vars = Set{Int}()
433+
# Equations in `mm` of the form `var ~ 0`
434+
zero_eqs = Set{Int}()
435+
# Equations that `zero_vars` are incident on and need to be substituted
436+
eqs_to_substitute = Set{Int}()
437+
# Queue of indices in `mm.nzrows` containing rows to check for being
438+
# `var ~ 0`
439+
queue = Queue{Int}()
440+
# Initially check all rows with just one element
441+
for i in eachindex(mm.nzrows)
442+
isone(length(mm.row_cols[i])) && push!(queue, i)
443+
end
444+
445+
while !isempty(queue)
446+
row_i = popfirst!(queue)
447+
eqidx = mm.nzrows[row_i]
448+
# Skip rows we already processed in case they show up twice
449+
eqidx in zero_eqs && continue
450+
451+
# If a row only contains one non-zero element, that element is also zero.
452+
# We filter `rcol` and `rval` to remove any zero elements. This could be
453+
# done in the `for nbor in nbors` loop below. However, it's possible we process
454+
# two zero variables both present in the same row before processing that row.
455+
# This approach avoids the row being processed by each of the two zero variables
456+
# individually in their `nbors` loop. We delay removing all zero elements to
457+
# the latest possible time to aggregate updates.
458+
rcol = mm.row_cols[row_i]
459+
rval = mm.row_vals[row_i]
460+
ptr = firstindex(rcol)
461+
nnz = 0
462+
for i in eachindex(rcol)
463+
rcol[i] in zero_vars && continue
464+
rcol[ptr] = rcol[i]
465+
rval[ptr] = rval[i]
466+
ptr = nextind(rcol, ptr)
467+
nnz += 1
468+
end
469+
resize!(rcol, nnz)
470+
resize!(rval, nnz)
471+
# `mm` does have rows with empty `rcol` on entry to this function, but they
472+
# should never be reachable by this search.
473+
@assert !iszero(nnz) "`mm` should not be singular at this point!"
474+
isone(nnz) || continue
475+
476+
zvar = rcol[1]
477+
push!(zero_vars, zvar)
478+
push!(zero_eqs, eqidx)
479+
480+
nbors = 𝑑neighbors(state.structure.graph, zvar)
481+
for nbor in nbors
482+
nzrow_idx = get(nzrow_to_idx, nbor, 0)
483+
# If `nbor` is not present in `mm`, it must be substituted later
484+
if iszero(nzrow_idx)
485+
push!(eqs_to_substitute, nbor)
486+
continue
487+
end
488+
# If it is present, process it
489+
push!(queue, nzrow_idx)
490+
end
491+
end
492+
493+
# Remove zero equations from `mm`
494+
if !isempty(zero_eqs)
495+
ptr = firstindex(mm.nzrows)
496+
for i in eachindex(mm.nzrows)
497+
eqidx = mm.nzrows[i]
498+
eqidx in zero_eqs && continue
499+
mm.nzrows[ptr] = mm.nzrows[i]
500+
mm.row_cols[ptr] = mm.row_cols[i]
501+
mm.row_vals[ptr] = mm.row_vals[i]
502+
ptr = nextind(mm.nzrows, ptr)
503+
end
504+
nnz = length(mm.nzrows) - length(zero_eqs)
505+
resize!(mm.nzrows, nnz)
506+
resize!(mm.row_cols, nnz)
507+
resize!(mm.row_vals, nnz)
508+
end
509+
510+
return zero_vars, zero_eqs, eqs_to_substitute
511+
end
512+
412513
function alias_elimination!(
413514
state::TearingState; fully_determined = true,
414515
print_underconstrained_variables = false, kwargs...
@@ -438,6 +539,10 @@ function alias_elimination!(
438539
fullvars_to_idx = Dict{SymbolicT, Int}(Iterators.map(reverse, enumerate(fullvars)))
439540
original_eqs = state.original_eqs
440541

542+
zero_vars, zero_eqs, eqs_to_substitute = eliminate_zero_variables!(state, mm)
543+
append!(vars_to_rm, zero_vars)
544+
append!(eqs_to_rm, zero_eqs)
545+
441546
for (ieq, eq) in enumerate(mm.nzrows)
442547
rcol = mm.row_cols[ieq]
443548
rval = mm.row_vals[ieq]
@@ -475,6 +580,18 @@ function alias_elimination!(
475580
end
476581
end
477582

583+
subrules = Dict{SymbolicT, SymbolicT}()
584+
sizehint!(subrules, length(zero_vars))
585+
for v in zero_vars
586+
subrules[fullvars[v]] = Symbolics.COMMON_ZERO
587+
push!(state.additional_observed, fullvars[v] ~ Symbolics.COMMON_ZERO)
588+
end
589+
subber = SU.IRSubstituter{true}(get_irstructure(sys), subrules)
590+
for ieq in eqs_to_substitute
591+
eqs[ieq] = subber(eqs[ieq])
592+
original_eqs[ieq] = subber(original_eqs[ieq])
593+
end
594+
478595
@set! sys.eqs = eqs
479596
state.sys = sys
480597

0 commit comments

Comments
 (0)