Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ QUBODrivers = "a3f166f7-2cd3-47b6-9e1e-6fbfe0449eb0"
QUBOTools = "60eb5b62-0a39-4ddc-84c5-97d2adff9319"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

[weakdeps]
SpinGlassEngine = "0563570f-ea1b-4080-8a64-041ac6565a4e"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question for the reviewer: these weakdeps require Julia 1.11, but TenSolver declares julia = "1.10". I confirmed SpinGlassNetworks@1.4 / SpinGlassTensors@1.3 are unresolvable on 1.10, so the extension can never load on the only CI-able Julia — the whole bridge is dead/untested on CI until compat is raised to 1.11. Optionality is correct (weakdeps, not deps), so not a blocker; decide whether to merge as experimental scaffolding (allowed by #38) or defer activation, and track a follow-up to exercise it under 1.11.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed in this pass. I am keeping this as experimental scaffolding rather than raising TenSolver's Julia compat or adding an activation path here. Follow-up remains to exercise the extension under Julia 1.11 or once the SpinGlass compat stack resolves.

SpinGlassNetworks = "b7f6bd3e-55dc-4da6-96a9-ef9dbec6ac19"
SpinGlassTensors = "7584fc6a-5a23-4eeb-8277-827aab0146ea"

[extensions]
TenSolverSpinGlassPEPSExt = ["SpinGlassEngine", "SpinGlassNetworks", "SpinGlassTensors"]

[compat]
Combinatorics = "1"
ITensorMPS = "0.3"
Expand All @@ -22,6 +30,9 @@ LinearAlgebra = "1.10"
MultivariatePolynomials = "0.5.9"
Printf = "1"
SparseArrays = "1.10"
SpinGlassEngine = "1.6"
SpinGlassNetworks = "1.4"
SpinGlassTensors = "1.3"
QUBODrivers = "0.6.1"
QUBOTools = "0.13"
julia = "1.10"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optionality verified: SpinGlass components are weakdeps with an extension entry, not hard deps, and compat bounds are set. This matches issue #38's requirement not to add SpinGlassPEPS as mandatory deps. Note for the rebase onto current main (v0.2.0 / 9a3dd5c): the QUBOTools compat widening to "0.10, 0.11" and the SpinGlass compat additions are likely conflict points against the post-#49/#53/#54 Project.toml.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed by c31f1e6. The restack keeps the SpinGlass weakdeps/compat and resolves the conflict onto the current QUBODrivers 0.6.1 / QUBOTools 0.13 base. Pkg.instantiate() passed.

1 change: 1 addition & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This page documents the public API of TenSolver.jl.
```@docs
TenSolver.minimize
TenSolver.maximize
TenSolver.solve_ising
```

## Solver Backends
Expand Down
27 changes: 25 additions & 2 deletions docs/src/spinglasspeps_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,31 @@ learn the SpinGlassPEPS API. The current behavior is:
- `backend = :dmrg` selects the current path explicitly;
- `backend = DMRGBackend()` selects the current path explicitly through the
backend-object interface; and
- `backend = :peps` errors clearly until a later optional bridge package or
extension provides the structured backend.
- unavailable backend symbols error clearly without changing default DMRG
behavior.

This stack step keeps the direct PEPS path as non-public scaffolding. The core
package contains internal backend, topology, and result boundaries, while the
exported `solve_ising` function is the public Ising boundary that optional
structured backends may implement. `TenSolverSpinGlassPEPSExt` owns the
SpinGlass component imports and calls. This keeps ordinary TenSolver installs
on the existing dependency footprint and avoids documenting an activation path
that cannot be tested from registered packages.

The extension remains gated while the upstream dependency stack settles. In
local checks against SpinGlassNetworks 1.4, SpinGlassEngine 1.6, and
SpinGlassTensors 1.3, the current registered component compat bounds do not
resolve with TenSolver's ITensors/QUBOTools environment. The source bridge and
gated tests are kept in this stack step so the TenSolver boundary is concrete,
but the PEPS backend types are not exported or listed in the public API until
CI can exercise the SpinGlass component stack.

The initial internal structured topology scaffolding covers one-spin-per-site
and multi-spin-per-site square/king grids. QUBO inputs are converted through
[`qubo_to_ising`](@ref) before the PEPS extension builds a SpinGlassNetworks
Ising graph, clusters it with `super_square_lattice`, constructs the Potts
Hamiltonian, runs `MpsContractor` plus `low_energy_spectrum`, and decodes
retained states back to TenSolver Boolean vectors.

Later PRs should add QUBODrivers/JuMP raw optimizer attributes for backend and
PEPS parameters.
Expand Down
263 changes: 263 additions & 0 deletions ext/TenSolverSpinGlassPEPSExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
module TenSolverSpinGlassPEPSExt

import SparseArrays: findnz

import TenSolver
import TenSolver:
IsingModel,
KingGrid,
PEPSBackend,
PEPSSolution,
SquareGrid,
ising_energy,
spin_to_bool

import SpinGlassEngine
import SpinGlassNetworks
import SpinGlassTensors

function _peps_type(::IsingModel{T}) where {T}
return typeof(float(one(T)))
end

function _ising_instance(model::IsingModel{T}) where {T}
S = _peps_type(model)
instance = Dict{Tuple{Int, Int}, S}()

for i in eachindex(model.h)
instance[(i, i)] = S(model.h[i])
end

rows, cols, vals = findnz(model.J)
for k in eachindex(vals)
i = rows[k]
j = cols[k]
if i < j && !iszero(vals[k])
instance[(i, j)] = get(instance, (i, j), zero(S)) + S(vals[k])
end
end

return instance
end

function _check_topology_size(topology, model::IsingModel)
expected = TenSolver._topology_size(topology)
actual = length(model.h)
actual == expected ||
throw(DimensionMismatch("PEPS topology $(repr(topology)) expects $expected spins, but the Ising model has $actual spins."))
end

_edge_supported(::SquareGrid, a::Tuple, b::Tuple) = abs(a[1] - b[1]) + abs(a[2] - b[2]) == 1
_edge_supported(::KingGrid, a::Tuple, b::Tuple) = maximum(abs.(a .- b)) == 1

function _check_layout_edges(topology, model::IsingModel, lattice)
rows, cols, vals = findnz(model.J)
for k in eachindex(vals)
i = rows[k]
j = cols[k]
if i < j && !iszero(vals[k])
ci = lattice[i]
cj = lattice[j]
if ci != cj && !_edge_supported(topology, ci, cj)
throw(ArgumentError(
"Ising coupling ($i, $j) is not compatible with $(repr(topology)). " *
"Use a compatible structured topology or backend = :dmrg."
))
end
end
end
end

function _potts_hamiltonian(backend::PEPSBackend, ig, lattice)
if isnothing(backend.local_dimension)
return SpinGlassNetworks.potts_hamiltonian(
ig;
spectrum = SpinGlassNetworks.full_spectrum,
cluster_assignment_rule = lattice,
)
end

return SpinGlassNetworks.potts_hamiltonian(
ig,
backend.local_dimension;
spectrum = SpinGlassNetworks.full_spectrum,
cluster_assignment_rule = lattice,
)
end

function _transformations(transformations)
transformations === :all && return SpinGlassEngine.all_lattice_transformations
transformations === :identity && return (SpinGlassEngine.rotation(0),)
if transformations isa Symbol
throw(ArgumentError("Unsupported PEPS transformations $(repr(transformations)). Use :all, :identity, a transformation, or a collection of transformations."))
end
transformations isa Tuple && return transformations
transformations isa AbstractVector && return Tuple(transformations)
return (transformations,)
end

function _strategy(backend::PEPSBackend)
backend.contraction in (:auto, :svd, :svd_truncate) && return SpinGlassEngine.SVDTruncate
backend.contraction === :zipper && return SpinGlassEngine.Zipper
throw(ArgumentError("Unsupported PEPS contraction $(repr(backend.contraction))."))
end

function _network(topology::SquareGrid, potts_h, transform, ::Type{T}) where {T}
return SpinGlassEngine.PEPSNetwork{
SpinGlassEngine.SquareSingleNode{SpinGlassEngine.GaugesEnergy},
SpinGlassEngine.Dense,
T,
}(topology.m, topology.n, potts_h, transform)
end

function _network(topology::KingGrid, potts_h, transform, ::Type{T}) where {T}
return SpinGlassEngine.PEPSNetwork{
SpinGlassEngine.KingSingleNode{SpinGlassEngine.GaugesEnergy},
SpinGlassEngine.Dense,
T,
}(topology.m, topology.n, potts_h, transform)
end

function _decoded_records(model::IsingModel, potts_h, sol, transform)
records = NamedTuple[]
for i in eachindex(sol.states)
decoded = SpinGlassNetworks.decode_potts_hamiltonian_state(potts_h, sol.states[i])
spins = [decoded[j] for j in eachindex(model.h)]
state = spin_to_bool(spins)
push!(records, (;
state,
spins,
energy = ising_energy(model, spins),
probability = sol.probabilities[i],
transformation = transform,
raw_energy = sol.energies[i],
))
end
return records
end

function _deduplicated_records(records)
sort!(records; by = r -> (r.energy, -r.probability))

deduped = NamedTuple[]
positions = Dict{Any, Int}()
for record in records
key = Tuple(record.state)
index = get(positions, key, nothing)
if isnothing(index)
push!(deduped, record)
positions[key] = lastindex(deduped)
else
existing = deduped[index]
deduped[index] = (; existing..., probability = existing.probability + record.probability)
end
end

return deduped
end

function _metadata(backend::PEPSBackend, records, raw_results, failures)
best = first(records)
raw = raw_results[best.transformation]
return Dict{String, Any}(
"backend" => "SpinGlassPEPS",
"topology" => TenSolver._topology_name(backend.topology),
"topology_size" => TenSolver._topology_tuple(backend.topology),
"beta" => backend.beta,
"bond_dim" => backend.bond_dim,
"max_states" => backend.max_states,
"cutoff_prob" => backend.cutoff_prob,
"onGPU" => backend.onGPU,
"contraction" => String(backend.contraction),
"num_sweeps" => backend.num_sweeps,
"graduate_truncation" => backend.graduate_truncation,
"local_dimension" => backend.local_dimension,
"transformations_tried" => collect(string.(keys(raw_results))),
"transformations_failed" => [string(failure.transformation) for failure in failures],
"selected_transformation" => string(best.transformation),
"spin_glass_energies" => collect(raw.solution.energies),
"spin_glass_probabilities" => collect(raw.solution.probabilities),
"largest_discarded_probability" => raw.solution.largest_discarded_probability,
)
end

function TenSolver._solve_ising(backend::PEPSBackend, model::IsingModel; cutoff = nothing, verbosity = 1, kwargs...)
if !isempty(kwargs)
names = join(string.(keys(kwargs)), ", ")
throw(ArgumentError("Unsupported PEPS backend keyword(s): $names. Configure PEPSBackend instead."))
end

_check_topology_size(backend.topology, model)

S = _peps_type(model)
instance = _ising_instance(model)
ig = SpinGlassNetworks.ising_graph(S, instance)
lattice = SpinGlassNetworks.super_square_lattice(TenSolver._topology_tuple(backend.topology))
_check_layout_edges(backend.topology, model, lattice)
potts_h = _potts_hamiltonian(backend, ig, lattice)
params = SpinGlassEngine.MpsParameters{S}(;
bond_dim = backend.bond_dim,
num_sweeps = backend.num_sweeps,
)
search_params = SpinGlassEngine.SearchParameters(;
max_states = backend.max_states,
cutoff_prob = backend.cutoff_prob,
)
strategy = _strategy(backend)

records = NamedTuple[]
raw_results = Dict{Any, Any}()
failures = NamedTuple[]
for transform in _transformations(backend.transformations)
try
net = _network(backend.topology, potts_h, transform, S)
ctr = SpinGlassEngine.MpsContractor(
strategy,
net,
params;
onGPU = backend.onGPU,
beta = S(backend.beta),
graduate_truncation = backend.graduate_truncation,
)
merge_strategy = SpinGlassEngine.merge_branches(ctr; merge_prob = :none)
sol, info = SpinGlassEngine.low_energy_spectrum(
ctr,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nonblocking: this loop uses try ... finally with no catch. With transformations = :all, a throw from any single transformation aborts the entire solve even if other transformations would have produced valid states. Consider recording per-transform failures and continuing, since #38 frames transformations as a set to try.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in edd04e4. The extension now records per-transform failures, continues to later transformations, clears cache in finally, and only throws after the loop if no records were produced.

search_params,
merge_strategy;
no_cache = backend.no_cache,
)

raw_results[transform] = (; solution = sol, info)
append!(records, _decoded_records(model, potts_h, sol, transform))
catch err
push!(failures, (;
transformation = transform,
error = sprint(showerror, err),
))
verbosity > 0 && @warn "SpinGlassPEPS transformation failed" transformation = transform exception = (err, catch_backtrace())
finally
SpinGlassEngine.clear_memoize_cache()
end
end

if isempty(records)
if isempty(failures)
throw(ArgumentError("SpinGlassPEPS did not return any states."))
end

failure_summary = join(("$(failure.transformation): $(failure.error)" for failure in failures), "; ")
throw(ArgumentError("SpinGlassPEPS did not return any states. Failed transformations: $failure_summary"))
end
records = _deduplicated_records(records)
states = [record.state for record in records]
energies = S[record.energy for record in records]
probabilities = S[record.probability for record in records]
metadata = _metadata(backend, records, raw_results, failures)
raw = (; results = raw_results, failures)

verbosity > 0 && @info "SpinGlassPEPS backend finished" energy = first(energies) states = length(states)

return first(energies), PEPSSolution{S}(states, energies, probabilities, metadata, raw)
end

end # module
2 changes: 1 addition & 1 deletion src/TenSolver.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ include("ising.jl")
export bool_to_spin, spin_to_bool, qubo_to_ising, ising_to_qubo

include("solver.jl")
export minimize, maximize
export minimize, maximize, solve_ising
export AbstractTenSolverBackend, DMRGBackend

# Convergence logging
Expand Down
Loading
Loading