From fa357d33f4fe7ff5e8b964f49081e159673dffe5 Mon Sep 17 00:00:00 2001 From: Luis-Varona Date: Sat, 8 Nov 2025 19:27:21 -0400 Subject: [PATCH] Finalized (tentatively) state transfer code; updated docs We finalize the code for state transfer, one of the three phenomena on quantum networks which this package investigates (the other two being uniform mixing and fractional revival, although we may or may not add more in the future). Docstrings are set up but not yet finalized; and on that note, we update the (also unfinished) docs in the EpsilonOptimization submodule as well. --- .../EpsilonOptimization.jl | 1 + .../solvers/alpha_branch_and_bound.jl | 36 +- .../solvers/lipschitz_branch_and_bound.jl | 38 +- src/EpsilonOptimization/types.jl | 23 +- src/EpsilonOptimization/utils.jl | 37 ++ src/QuantumStateTransfer.jl | 7 +- src/{core.jl => fractional_revival.jl} | 0 src/state_transfer.jl | 379 ++++++++++++++++++ src/types.jl | 4 +- src/uniform_mixing.jl | 7 + src/utils.jl | 51 ++- 11 files changed, 548 insertions(+), 35 deletions(-) create mode 100644 src/EpsilonOptimization/utils.jl rename src/{core.jl => fractional_revival.jl} (100%) create mode 100644 src/state_transfer.jl create mode 100644 src/uniform_mixing.jl diff --git a/src/EpsilonOptimization/EpsilonOptimization.jl b/src/EpsilonOptimization/EpsilonOptimization.jl index a880a00..54ef2f5 100644 --- a/src/EpsilonOptimization/EpsilonOptimization.jl +++ b/src/EpsilonOptimization/EpsilonOptimization.jl @@ -31,6 +31,7 @@ export AlphaBranchAndBound include("types.jl") +include("utils.jl") include("core.jl") include("solvers/lipschitz_branch_and_bound.jl") diff --git a/src/EpsilonOptimization/solvers/alpha_branch_and_bound.jl b/src/EpsilonOptimization/solvers/alpha_branch_and_bound.jl index 855a631..7a5f9bd 100644 --- a/src/EpsilonOptimization/solvers/alpha_branch_and_bound.jl +++ b/src/EpsilonOptimization/solvers/alpha_branch_and_bound.jl @@ -11,9 +11,25 @@ """ struct AlphaBranchAndBound <: AbstractEpsilonSolver epsilon::Real - threshold::Union{<:Real,Nothing} + target::Union{<:Real,Nothing} max_iterations::Union{<:Integer,Nothing} alpha::Real + + function AlphaBranchAndBound( + epsilon::Real, + alpha::Real; + target::Union{<:Real,Nothing}=nothing, + max_iterations::Union{<:Integer,Nothing}=nothing, + ) + solver = new(epsilon, target, max_iterations, alpha) + validate_solver_params(solver) + + if alpha <= 0 + throw(ArgumentError("Alpha parameter must be positive, got $alpha")) + end + + return solver + end end """ @@ -30,7 +46,7 @@ struct ABBHyperrectangle{Tx<:AbstractVector{<:Real},Tf<:Real} function ABBHyperrectangle( lower::Tx, upper::Tx, f::Function, alpha::Real ) where {Tx<:AbstractVector{<:Real}} - f_convex(x::Tx) = f(x) + alpha * sum((x .- lower) .* (upper .- x)) + f_convex(x::Tx) = f(x) - alpha * sum((x .- lower) .* (upper .- x)) x0 = lower .+ (upper .- lower) ./ 2 res = optimize(f_convex, lower, upper, x0, Fminbox(LBFGS())) @@ -51,10 +67,10 @@ function _epsilon_minimize_impl( epsilon = solver.epsilon alpha = solver.alpha - if isnothing(solver.threshold) - threshold = -Inf + if isnothing(solver.target) + target = -Inf else - threshold = solver.threshold + target = solver.target end if isnothing(solver.max_iterations) @@ -76,9 +92,9 @@ function _epsilon_minimize_impl( iterations < max_iterations && !isempty(rects_cand) && ( - isinf(threshold) || ( - min(lower_bound, first(rects_cand).lower_bound) <= threshold && - minimum - threshold >= epsilon + isinf(target) || ( + min(lower_bound, first(rects_cand).lower_bound) <= target && + minimum - target >= epsilon ) ) ) @@ -123,8 +139,8 @@ function _epsilon_minimize_impl( minimum, ( epsilon_optimal=minimum - lower_bound < epsilon, - threshold_reached=minimum - threshold < epsilon, - threshold_unreachable=-Inf < threshold < lower_bound, + target_reached=minimum - target < epsilon, + target_unreachable=-Inf < target < lower_bound, iterations=iterations >= max_iterations, ), ) diff --git a/src/EpsilonOptimization/solvers/lipschitz_branch_and_bound.jl b/src/EpsilonOptimization/solvers/lipschitz_branch_and_bound.jl index 2e33fc4..182a4d0 100644 --- a/src/EpsilonOptimization/solvers/lipschitz_branch_and_bound.jl +++ b/src/EpsilonOptimization/solvers/lipschitz_branch_and_bound.jl @@ -11,9 +11,29 @@ """ struct LipschitzBranchAndBound <: AbstractEpsilonSolver epsilon::Real - threshold::Union{<:Real,Nothing} + target::Union{<:Real,Nothing} max_iterations::Union{<:Integer,Nothing} lipschitz_constant::Real + + function LipschitzBranchAndBound( + epsilon::Real, + lipschitz_constant::Real; + target::Union{<:Real,Nothing}=nothing, + max_iterations::Union{<:Integer,Nothing}=nothing, + ) + solver = new(epsilon, target, max_iterations, lipschitz_constant) + validate_solver_params(solver) + + if lipschitz_constant <= 0 + throw( + ArgumentError( + "Lipschitz constant must be positive, got $lipschitz_constant" + ), + ) + end + + return solver + end end """ @@ -49,10 +69,10 @@ function _epsilon_minimize_impl( epsilon = solver.epsilon lipschitz_constant = solver.lipschitz_constant - if isnothing(solver.threshold) - threshold = -Inf + if isnothing(solver.target) + target = -Inf else - threshold = solver.threshold + target = solver.target end if isnothing(solver.max_iterations) @@ -74,9 +94,9 @@ function _epsilon_minimize_impl( iterations < max_iterations && !isempty(rects_cand) && ( - isinf(threshold) || ( - min(lower_bound, first(rects_cand).lower_bound) <= threshold && - minimum - threshold >= epsilon + isinf(target) || ( + min(lower_bound, first(rects_cand).lower_bound) <= target && + minimum - target >= epsilon ) ) ) @@ -118,8 +138,8 @@ function _epsilon_minimize_impl( minimum, ( epsilon_optimal=minimum - lower_bound < epsilon, - threshold_reached=minimum - threshold < epsilon, - threshold_unreachable=-Inf < threshold < lower_bound, + target_reached=minimum - target < epsilon, + target_unreachable=-Inf < target < lower_bound, iterations=iterations >= max_iterations, ), ) diff --git a/src/EpsilonOptimization/types.jl b/src/EpsilonOptimization/types.jl index 5de5ad7..a28517d 100644 --- a/src/EpsilonOptimization/types.jl +++ b/src/EpsilonOptimization/types.jl @@ -16,9 +16,9 @@ Concrete subtypes of `AbstractEpsilonSolver` must implement the following method Concrete subtypes of `AbstractEpsilonSolver` must also have the following fields: - `epsilon::Real`: The tolerance for ``ϵ``-convergence. -- `threshold::Union{<:Real,Nothing}`: An optional threshold value resulting in termination - once either (1) a function value less than `threshold + epsilon` is found or (2) it is - determined that no function value less than or equal to `threshold` exists. +- `target::Union{<:Real,Nothing}`: An optional threshold value resulting in termination once + either (1) a function value less than `target + epsilon` is found or (2) it is + determined that no function value less than or equal to `target` exists. - `max_iterations::Union{<:Integer,Nothing}`: An optional maximum number of iterations after which the algorithm will terminate. """ @@ -38,10 +38,10 @@ Output struct for ``ϵ``-optimization results. - `minimum::Tf<:Real`: The estimated global minimum function value. - `stopped_by::NamedTuple`: The reason(s) for termination, namely: - `:epsilon_optimal::Bool`: Whether ``ϵ``-optimality was achieved. - - `:threshold_reached::Bool`: Whether ta function value less than `threshold + epsilon` - was found for some optionally provided `threshold`. - - `:threshold_unreachable::Bool`: Whether it was determined that no function value less - than or equal to `threshold` existed for some optionally provided `threshold`. + - `:target_reached::Bool`: Whether ta function value less than `target + epsilon` was + found for some optionally provided `target`. + - `:target_unreachable::Bool`: Whether it was determined that no function value less + than or equal to `target` existed for some optionally provided `target`. - `:iterations::Bool`: Whether termination occurred due to reaching the maximum number of iterations for some optionally provided `max_iterations`. """ @@ -53,17 +53,16 @@ struct EpsilonMinimizationResult{Tx<:AbstractVector{<:Real},Tf<:Real} minimizer::Tx minimum::Tf stopped_by::NamedTuple{ - (:epsilon_optimal, :threshold_reached, :threshold_unreachable, :iterations), + (:epsilon_optimal, :target_reached, :target_unreachable, :iterations), Tuple{Bool,Bool,Bool,Bool}, } end -# The `Base.show` override here takes heavy inspiration from the Optim.jl package function Base.show(io::IO, res::EpsilonMinimizationResult) println(io, "Results of Epsilon Minimization Algorithm") print(io, " * Status: ") - if res.stopped_by.epsilon_optimal || res.stopped_by.threshold_reached + if res.stopped_by.epsilon_optimal || res.stopped_by.target_reached println(io, "success") else println(io, "failure") @@ -73,8 +72,8 @@ function Base.show(io::IO, res::EpsilonMinimizationResult) println(io, " * Lower bounds: $(res.lower)") println(io, " * Upper bounds: $(res.upper)") println(io, " * Epsilon tolerance: $(res.epsilon)") - println(io, " * Estimated minimizer: $(res.minimizer)") - println(io, " * Estimated minimum: $(res.minimum)") + println(io, " * Minimizer: $(res.minimizer)") + println(io, " * Minimum: $(res.minimum)") return nothing end diff --git a/src/EpsilonOptimization/utils.jl b/src/EpsilonOptimization/utils.jl new file mode 100644 index 0000000..2b5413f --- /dev/null +++ b/src/EpsilonOptimization/utils.jl @@ -0,0 +1,37 @@ +# Copyright 2025 Luis M. B. Varona and Nathaniel Johnston +# +# Licensed under the MIT license . This file may not be copied, modified, or +# distributed except according to those terms. + +""" + validate_solver_params(solver) + +[TODO: Write here] + +# Arguments +[TODO: Write here] + +# Raises +[TODO: Write here] + +# Returns +[TODO: Write here] +""" +function validate_solver_params(solver::AbstractEpsilonSolver) + epsilon = solver.epsilon + + if epsilon <= 0 + throw(ArgumentError("Epsilon parameter must be positive, got $epsilon")) + end + + max_iterations = solver.max_iterations + + if !isnothing(max_iterations) && max_iterations <= 0 + throw( + ArgumentError( + "Max iterations must be positive or nothing, got $(max_iterations)" + ), + ) + end +end diff --git a/src/QuantumStateTransfer.jl b/src/QuantumStateTransfer.jl index 8b36be3..7e396e3 100644 --- a/src/QuantumStateTransfer.jl +++ b/src/QuantumStateTransfer.jl @@ -22,9 +22,12 @@ include("types.jl") include("EpsilonOptimization/EpsilonOptimization.jl") using .EpsilonOptimization -include("core.jl") +include("state_transfer.jl") +include("uniform_mixing.jl") +include("fractional_revival.jl") -# TODO: Exports +# TODO: Exports (add more later) +export max_state_transfer, check_state_transfer include("startup.jl") diff --git a/src/core.jl b/src/fractional_revival.jl similarity index 100% rename from src/core.jl rename to src/fractional_revival.jl diff --git a/src/state_transfer.jl b/src/state_transfer.jl new file mode 100644 index 0000000..b616831 --- /dev/null +++ b/src/state_transfer.jl @@ -0,0 +1,379 @@ +# Copyright 2025 Luis M. B. Varona and Nathaniel Johnston +# +# +# Licensed under the MIT license . This file may not be copied, modified, or +# distributed except according to those terms. + +""" + StateTransferMaximizationResult{Tn,Ts} + +[TODO: Write here] +""" +struct StateTransferMaximizationResult{ + Tn<:Union{AbstractGraph,Matrix{Float64}},Ts<:Union{Int,Tuple{Int,Int}} +} + network::Tn + src::Ts + dst::Ts + t_lower::Float64 + t_upper::Float64 + epsilon::Float64 + maximizer::Float64 + max_fidelity::Float64 +end + +function Base.show(io::IO, res::StateTransferMaximizationResult{Tn,Int}) where {Tn} + println(io, "Vertex State Transfer Maximization:") + println(io, " * Network: $(summary(res.network))") + println(io, " * Source vertex: $(res.src)") + println(io, " * Destination vertex: $(res.dst)") + println(io, " * Time interval: [$(res.t_lower), $(res.t_upper)]") + println(io, " * Epsilon tolerance: $(res.epsilon)") + println(io, " * Maximizing time: $(res.maximizer)") + println(io, " * Maximum fidelity: $(res.max_fidelity)") + + return nothing +end + +function Base.show( + io::IO, res::StateTransferMaximizationResult{Tn,Tuple{Int,Int}} +) where {Tn} + println(io, "Pair State Transfer Maximization:") + println(io, " * Network: $(summary(res.network))") + println(io, " * Source vertices: $(res.src)") + println(io, " * Destination vertices: $(res.dst)") + println(io, " * Time interval: [$(res.t_lower), $(res.t_upper)]") + println(io, " * Epsilon tolerance: $(res.epsilon)") + println(io, " * Maximizing time: $(res.maximizer)") + println(io, " * Maximum fidelity: $(res.max_fidelity)") + + return nothing +end + +""" + StateTransferRecognitionResult{Tn,Ts} + +[TODO: Write here] +""" +struct StateTransferRecognitionResult{ + Tn<:Union{AbstractGraph,Matrix{Float64}},Ts<:Union{Int,Tuple{Int,Int}} +} + network::Tn + src::Ts + dst::Ts + t_lower::Float64 + t_upper::Float64 + epsilon::Float64 + target_fidelity::Float64 + achieved::Bool + time_achieved::Union{Nothing,Float64} + fidelity_achieved::Union{Nothing,Float64} +end + +function Base.show(io::IO, res::StateTransferRecognitionResult{Tn,Int}) where {Tn} + println(io, "Vertex State Transfer Recognition:") + println(io, " * Network: $(summary(res.network))") + println(io, " * Source vertex: $(res.src)") + println(io, " * Destination vertex: $(res.dst)") + println(io, " * Time interval: [$(res.t_lower), $(res.t_upper)]") + println(io, " * Epsilon tolerance: $(res.epsilon)") + println(io, " * Target fidelity: $(res.target_fidelity)") + println(io, " * Achieved: $(res.achieved)") + + if res.achieved + println(io, " * Time achieved: $(res.time_achieved)") + println(io, " * Fidelity achieved: $(res.fidelity_achieved)") + end + + return nothing +end + +function Base.show( + io::IO, res::StateTransferRecognitionResult{Tn,Tuple{Int,Int}} +) where {Tn} + println(io, "Pair State Transfer Recognition:") + println(io, " * Network: $(summary(res.network))") + println(io, " * Source vertices: $(res.src)") + println(io, " * Destination vertices: $(res.dst)") + println(io, " * Time interval: [$(res.t_lower), $(res.t_upper)]") + println(io, " * Epsilon tolerance: $(res.epsilon)") + println(io, " * Target fidelity: $(res.target_fidelity)") + println(io, " * Achieved: $(res.achieved)") + + if res.achieved + println(io, " * Time achieved: $(res.time_achieved)") + println(io, " * Fidelity achieved: $(res.fidelity_achieved)") + end + + return nothing +end + +""" + max_state_transfer(g::AbstractGraph, args...) -> StateTransferMaximizationResult + max_state_transfer(A::AbstractMatrix{<:Real}, args...) -> StateTransferMaximizationResult + +[TODO: Write here] + +# Arguments +[TODO: Write here] + +# Optional Arguments +[TODO: Write here] + +# Raises +[TODO: Write here] + +# Returns +[TODO: Write here] + +# Examples +[TODO: Write here] + +# Notes +[TODO: Write here. Proof sketch of bounds on Lipschitz and alpha constants, plus further +relevant references?] +""" +function max_state_transfer(g::AbstractGraph, args...) + if !is_simple(g) + throw(ArgumentError("Graph must be undirected with no self-loops")) + end + + return max_state_transfer(adjacency_matrix(g), args...) +end + +function max_state_transfer( + A::AbstractMatrix{<:Real}, + src::Tl, + dst::Tl, + t_lower::Real, + t_upper::Real, + epsilon::Real, + method::Symbol=:lipschitz_bb, +) where {Tl<:Union{Integer,Tuple{Integer,Integer}}} + if !is_zero_diagonal_symmetric(A) + throw(ArgumentError("Matrix must be symmetric with zero diagonal")) + end + + _validate_state_transfer_params(t_lower, t_upper, epsilon, method) + + input = _preprocess_state_transfer_input(A, src, dst, t_lower, t_upper, epsilon, method) + res = _optimize_state_transfer_impl(input) + + return StateTransferMaximizationResult( + input.A, + input.src, + input.dst, + input.t_lower, + input.t_upper, + input.epsilon, + res.minimizer[1], + 1 - res.minimum, + ) +end + +""" + check_state_transfer(g::AbstractGraph, args...) -> StateTransferRecognitionResult + check_state_transfer(A::AbstractMatrix{<:Real}, args...) -> StateTransferRecognitionResult + +[TODO: Write here] + +# Arguments +[TODO: Write here] + +# Optional Arguments +[TODO: Write here] + +# Raises +[TODO: Write here] + +# Returns +[TODO: Write here] + +# Examples +[TODO: Write here] + +# Notes +[TODO: Write here. Proof sketch of bounds on Lipschitz and alpha constants, plus further +relevant references?] +""" +function check_state_transfer(g::AbstractGraph, args...) + if !is_simple(g) + throw(ArgumentError("Graph must be undirected with no self-loops")) + end + + return check_state_transfer(adjacency_matrix(g), args...) +end + +function check_state_transfer( + A::AbstractMatrix{<:Real}, + src::Tl, + dst::Tl, + t_lower::Real, + t_upper::Real, + target_fidelity::Real, + epsilon::Real, + method::Symbol=:lipschitz_bb, +) where {Tl<:Union{Integer,Tuple{Integer,Integer}}} + if !is_zero_diagonal_symmetric(A) + throw(ArgumentError("Matrix must be symmetric with zero diagonal")) + end + + _validate_state_transfer_params(t_lower, t_upper, epsilon, method, target_fidelity) + + input = _preprocess_state_transfer_input( + A, src, dst, t_lower, t_upper, epsilon, method, target_fidelity + ) + res = _optimize_state_transfer_impl(input) + + achieved = res.target_reached + + if achieved + time_achieved = res.minimizer[1] + fidelity_achieved = 1 - res.minimum + else + time_achieved = nothing + fidelity_achieved = nothing + end + + return StateTransferRecognitionResult( + input.A, + input.src, + input.dst, + input.t_lower, + input.t_upper, + input.epsilon, + target_fidelity, + achieved, + time_achieved, + fidelity_achieved, + ) +end + +struct _StateTransferProblemInput{Tl<:Union{Int,Tuple{Int,Int}}} + A::Matrix{Float64} + src::Tl + dst::Tl + t_lower::Float64 + t_upper::Float64 + epsilon::Float64 + method::Symbol + target_fidelity::Union{Nothing,Float64} +end + +function _validate_state_transfer_params( + t_lower::Real, + t_upper::Real, + epsilon::Real, + method::Symbol, + target_fidelity::Union{Nothing,Float64}=nothing, +) + if t_lower > t_upper + throw( + ArgumentError( + "Lower time bound must be less than or equal to upper bound, got [$t_lower, $t_upper]", + ), + ) + end + + if epsilon <= 0 + throw(ArgumentError("Epsilon tolerance must be positive, got $epsilon")) + end + + if !(method in (:lipschitz_bb, :alpha_bb)) + throw( + ArgumentError( + "Unsupported epsilon-convergent optimization method; must be `:lipschitz_bb` or `:alpha_bb`, got $method", + ), + ) + end + + if !isnothing(target_fidelity) + if !(0 < target_fidelity <= 1) + throw( + ArgumentError( + "Target fidelity must be in the interval (0, 1], got $target_fidelity" + ), + ) + end + end +end + +function _preprocess_state_transfer_input( + A::AbstractMatrix{<:Real}, + src::Tl, + dst::Tl, + t_lower::Real, + t_upper::Real, + epsilon::Real, + method::Symbol, + target_fidelity::Union{Nothing,Float64}=nothing, +) where {Tl<:Union{Integer,Tuple{Integer,Integer}}} + A = Matrix{Float64}(A) + src = _preprocess_label(src) + dst = _preprocess_label(dst) + t_lower = Float64(t_lower) + t_upper = Float64(t_upper) + epsilon = Float64(epsilon) + + return _StateTransferProblemInput( + A, src, dst, t_lower, t_upper, epsilon, method, target_fidelity + ) +end + +_preprocess_label(label::Integer) = Int(label) +_preprocess_label(label::Tuple{Integer,Integer}) = Tuple{Int,Int}(label) + +_label_to_state(label::Int, buf::UnitRange{Int}) = buf .== label + +function _label_to_state(label::Tuple{Int,Int}, buf::UnitRange{Int}) + if allequal(label) + throw(ArgumentError("Vertex pairs must consist of distinct vertices")) + end + + return ((buf .== label[1]) - (buf .== label[2])) / sqrt(2) +end + +function _optimize_state_transfer_impl(input::_StateTransferProblemInput) + A = input.A + t_lower = input.t_lower + t_upper = input.t_upper + method = input.method + epsilon = input.epsilon + + buf = 1:size(A, 1) + state_src = _label_to_state(input.src, buf) + state_dst = _label_to_state(input.dst, buf) + + if isnothing(input.target_fidelity) + target_infidelity = nothing + else + target_infidelity = 1 - input.target_fidelity + end + + if method == :lipschitz_bb + lipschitz_constant = maximum(norm.(eachcol(A))) + solver = LipschitzBranchAndBound( + epsilon, lipschitz_constant; target=target_infidelity + ) + elseif method == :alpha_bb + alpha = maximum(norm.(eachcol(A^2))) / 2 + solver = AlphaBranchAndBound(epsilon, alpha; target=target_infidelity) + else + throw( + ArgumentError( + "Unsupported epsilon-convergent optimization method; must be `:lipschitz_bb` or `:alpha_bb`", + ), + ) + end + + eigenvals, eigenvecs = eigen(A) + left = state_src' * eigenvecs + right = eigenvecs' * state_dst + + function infidelity(t::Vector{Float64}) + return 1 - abs2(left * Diagonal(exp.(im * t[1] * eigenvals)) * right) + end + + return epsilon_minimize(infidelity, [t_lower], [t_upper], solver) +end diff --git a/src/types.jl b/src/types.jl index 018e7fc..fce311c 100644 --- a/src/types.jl +++ b/src/types.jl @@ -38,7 +38,9 @@ called on a newly created subtype for which no method has been defined. `NotImplementedError` <: `Exception` # Notes -This struct was taken from one of the authors' other packages, MatrixBandwidth.jl. +This struct was taken from one of the authors' other packages, +[MatrixBandwidth.jl](https://github.com/Luis-Varona/MatrixBandwidth.jl). To avoid adding an +unnecessary dependency, it has been copied here verbatim. """ struct NotImplementedError{T<:Union{Nothing,Symbol}} <: Exception f::Function diff --git a/src/uniform_mixing.jl b/src/uniform_mixing.jl new file mode 100644 index 0000000..fa351df --- /dev/null +++ b/src/uniform_mixing.jl @@ -0,0 +1,7 @@ +# Copyright 2025 Luis M. B. Varona and Nathaniel Johnston +# +# Licensed under the MIT license . This file may not be copied, modified, or +# distributed except according to those terms. + +# TODO: Write here diff --git a/src/utils.jl b/src/utils.jl index fa351df..4ea82a3 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -4,4 +4,53 @@ # http://opensource.org/licenses/MIT>. This file may not be copied, modified, or # distributed except according to those terms. -# TODO: Write here +""" + is_zero_diagonal_symmetric(A) -> Bool + +Check whether a matrix `A` is symmetric with a zero diagonal. + +Supposing that `A` is the adjacency matrix of a graph representing a quantum spin network, a +nonzero diagonal would indicate couplings between qubits and themselves, which is physically +nonsensical. On the other hand, symmetry (or rather Hermicity in the general case, but we +only consider here real-valued adjacency matrices) is required for the walk Hamiltonian +``eᶦᵗᴬ`` to be unitary. + +# Arguments +- `A::AbstractMatrix{<:Real}`: The matrix to check. + +# Returns +- `::Bool`: `true` if `A` is symmetric with a zero diagonal, and `false` otherwise. + +# Examples +[TODO: Write here] +""" +function is_zero_diagonal_symmetric(A::AbstractMatrix{<:Real}) + (m, n) = size(A) + + return m == n && # Square + all(iszero, Iterators.map(i -> A[i, i], 1:n)) && # Zero diagonal + all(A[i, j] == A[j, i] for i in 1:(n - 1) for j in (i + 1):n) # Symmetric +end + +""" + is_simple(g) -> Bool + +Check whether a graph `g` is simple (i.e., undirected with no self-loops). + +Supposing that `g` represents a quantum spin network, self-loops would indicate couplings +between qubits and themselves, which is physically nonsensical. On the other hand, +undirectedness is required for the walk Hamiltonian ``eᶦᵗᴬ`` to be unitary, where `A` is the +adjacency matrix of `g`. + +# Arguments +- `g::AbstractGraph`: The graph to check. + +# Returns +- `::Bool`: `true` if `g` is undirected with no self-loops, and `false` otherwise. + +# Examples +[TODO: Write here] +""" +function is_simple(g::AbstractGraph) + return !isdirected(g) && !has_self_loops(g) +end