-
Notifications
You must be signed in to change notification settings - Fork 0
Add optional SpinGlassPEPS backend #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/issue-37-backend-interface
Are you sure you want to change the base?
Changes from all commits
ca890fc
e122868
f3c4db1
c31f1e6
edd04e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
| SpinGlassNetworks = "b7f6bd3e-55dc-4da6-96a9-ef9dbec6ac19" | ||
| SpinGlassTensors = "7584fc6a-5a23-4eeb-8277-827aab0146ea" | ||
|
|
||
| [extensions] | ||
| TenSolverSpinGlassPEPSExt = ["SpinGlassEngine", "SpinGlassNetworks", "SpinGlassTensors"] | ||
|
|
||
| [compat] | ||
| Combinatorics = "1" | ||
| ITensorMPS = "0.3" | ||
|
|
@@ -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" | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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, | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nonblocking: this loop uses
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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 | ||
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.