diff --git a/Project.toml b/Project.toml index 4ad992171..897bb12c8 100644 --- a/Project.toml +++ b/Project.toml @@ -13,6 +13,7 @@ Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" KeywordDispatch = "5888135b-5456-5c80-a1b6-c91ef8180460" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" OMEinsum = "ebe7aa44-baf0-506c-a96f-8464559b3922" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ScopedValues = "7e506255-f358-4e82-b7e4-beb19740aa63" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -76,6 +77,7 @@ KrylovKit = "0.7, 0.8" LinearAlgebra = "1.10" Makie = "0.18,0.19,0.20, 0.21" OMEinsum = "0.7, 0.8" +OrderedCollections = "1.6.3" PythonCall = "0.9" Quac = "0.3" Random = "1.10" diff --git a/demo.ipynb b/demo.ipynb new file mode 100644 index 000000000..27823ea36 --- /dev/null +++ b/demo.ipynb @@ -0,0 +1,340 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "using Tenet" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "true" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "istextmime(MIME\"application/vnd.vegalite.v3+json\"())" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.vegalite.v3+json": { + "$schema": "https://vega.github.io/schema/vega-lite/v3.json", + "data": { + "values": [ + { + "a": "A", + "b": 28 + }, + { + "a": "B", + "b": 55 + }, + { + "a": "C", + "b": 43 + }, + { + "a": "D", + "b": 91 + }, + { + "a": "E", + "b": 81 + }, + { + "a": "F", + "b": 53 + }, + { + "a": "G", + "b": 19 + }, + { + "a": "H", + "b": 87 + }, + { + "a": "I", + "b": 52 + } + ] + }, + "description": "A simple bar chart with embedded data.", + "encoding": { + "x": { + "axis": { + "labelAngle": 0 + }, + "field": "a", + "type": "nominal" + }, + "y": { + "field": "b", + "type": "quantitative" + } + }, + "mark": "bar" + }, + "text/plain": [ + "TensorNetwork (#tensors=10, #inds=25)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "tn = rand(TensorNetwork, 10, 5)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "true" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "showable(MIME\"application/vnd.vegalite.v3+json\"(), tn)" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [], + "source": [ + "struct E end\n", + "\n", + "function Base.show(io::IO, ::MIME\"application/vnd.vegalite.v3+json\", ::E)\n", + " println(io, \"\"\"{\n", + " \"\\$schema\": \"https://vega.github.io/schema/vega-lite/v3.json\",\n", + " \"data\": {\n", + " \"values\": [\n", + " {\"a\": \"C\", \"b\": 2},\n", + " {\"a\": \"C\", \"b\": 7},\n", + " {\"a\": \"C\", \"b\": 4},\n", + " {\"a\": \"D\", \"b\": 1},\n", + " {\"a\": \"D\", \"b\": 2},\n", + " {\"a\": \"D\", \"b\": 6},\n", + " {\"a\": \"E\", \"b\": 8},\n", + " {\"a\": \"E\", \"b\": 4},\n", + " {\"a\": \"E\", \"b\": 7}\n", + " ]\n", + " },\n", + " \"mark\": \"point\",\n", + " \"encoding\": {}\n", + " }\"\"\")\n", + "end" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "true" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "showable(MIME\"application/vnd.vegalite.v3+json\"(), E())" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.vegalite.v3+json": { + "$schema": "https://vega.github.io/schema/vega-lite/v3.json", + "data": { + "values": [ + { + "a": "C", + "b": 2 + }, + { + "a": "C", + "b": 7 + }, + { + "a": "C", + "b": 4 + }, + { + "a": "D", + "b": 1 + }, + { + "a": "D", + "b": 2 + }, + { + "a": "D", + "b": 6 + }, + { + "a": "E", + "b": 8 + }, + { + "a": "E", + "b": 4 + }, + { + "a": "E", + "b": 7 + } + ] + }, + "encoding": {}, + "mark": "point" + }, + "text/plain": [ + "E()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "E()" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.vegalite.v3+json": { + "$schema": "https://vega.github.io/schema/vega-lite/v3.json", + "data": { + "values": [ + { + "a": "A", + "b": 28 + }, + { + "a": "B", + "b": 55 + }, + { + "a": "C", + "b": 43 + }, + { + "a": "D", + "b": 91 + }, + { + "a": "E", + "b": 81 + }, + { + "a": "F", + "b": 53 + }, + { + "a": "G", + "b": 19 + }, + { + "a": "H", + "b": 87 + }, + { + "a": "I", + "b": 52 + } + ] + }, + "description": "A simple bar chart with embedded data.", + "encoding": { + "x": { + "axis": { + "labelAngle": 0 + }, + "field": "a", + "type": "nominal" + }, + "y": { + "field": "b", + "type": "quantitative" + } + }, + "mark": "bar" + }, + "text/plain": [ + "TensorNetwork (#tensors=10, #inds=25)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "tn" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 1.10.5", + "language": "julia", + "name": "julia-1.10" + }, + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.10.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/Project.toml b/docs/Project.toml index b2c25ef34..063e50ef4 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -5,7 +5,6 @@ DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" EinExprs = "b1794770-133b-4de1-afb4-526377e9f4c5" GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" -Kroki = "b3565e16-c1f2-4fe9-b4ab-221c88942068" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" NetworkLayout = "46757867-2c16-5918-afeb-47bfcb05e46a" Tenet = "85d41934-b9cd-44e1-8730-56d86f15f3ec" diff --git a/docs/make.jl b/docs/make.jl index 0474b13f7..238f291b1 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,8 +2,7 @@ using Documenter using DocumenterVitepress using DocumenterCitations using Tenet -using CairoMakie -using GraphMakie +using Makie using LinearAlgebra DocMeta.setdocmeta!(Tenet, :DocTestSetup, :(using Tenet); recursive=true) diff --git a/docs/package.json b/docs/package.json index 5633b4976..f6f59e4c0 100644 --- a/docs/package.json +++ b/docs/package.json @@ -5,8 +5,10 @@ "docs:preview": "vitepress preview build/.documenter" }, "dependencies": { + "@kazumatu981/markdown-it-kroki": "^1.3.5", "@shikijs/transformers": "^1.1.7", "markdown-it": "^14.1.0", + "markdown-it-abbr": "^2.0.0", "markdown-it-footnote": "^4.0.0", "markdown-it-mathjax3": "^4.3.2", "vitepress": "^1.1.4", diff --git a/docs/src/.vitepress/config.mts b/docs/src/.vitepress/config.mts index bdb14a1c3..eaa384e5a 100644 --- a/docs/src/.vitepress/config.mts +++ b/docs/src/.vitepress/config.mts @@ -2,6 +2,8 @@ import { defineConfig } from 'vitepress' import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' import mathjax3 from "markdown-it-mathjax3"; import footnote from "markdown-it-footnote"; +import kroki from "@kazumatu981/markdown-it-kroki"; +import abbr from "markdown-it-abbr"; const baseTemp = { base: 'REPLACE_ME_DOCUMENTER_VITEPRESS',// TODO: replace this in makedocs! @@ -36,9 +38,11 @@ export default defineConfig({ markdown: { math: true, config(md) { - md.use(tabsMarkdownPlugin), - md.use(mathjax3), - md.use(footnote) + md.use(tabsMarkdownPlugin) + md.use(mathjax3) + md.use(footnote) + md.use(kroki) + md.use(abbr) }, theme: { light: "github-light", diff --git a/docs/src/api/tensornetwork.md b/docs/src/api/tensornetwork.md index c2da8f50a..78a8a3a52 100644 --- a/docs/src/api/tensornetwork.md +++ b/docs/src/api/tensornetwork.md @@ -29,5 +29,5 @@ Tenet.ContractSimplification Tenet.DiagonalReduction Tenet.AntiDiagonalGauging Tenet.Truncate -Tenet.SplitSimplificationd +Tenet.SplitSimplification ``` diff --git a/docs/src/developer/type-hierarchy.md b/docs/src/developer/type-hierarchy.md index 4839859f7..a0a38cace 100644 --- a/docs/src/developer/type-hierarchy.md +++ b/docs/src/developer/type-hierarchy.md @@ -1,9 +1,5 @@ # Inheritance and Traits -```@setup kroki -using Kroki -``` - Julia (and in general, all modern languages like Rust or Go) implement Object Oriented Programming (OOP) in a rather restricted form compared to popular OOP languages like Java, C++ or Python. In particular, they forbid _structural inheritance_; i.e. inheriting fields from parent superclass(es). @@ -13,17 +9,17 @@ Julia design space on this topic is not completely clear. Julia has _abstract ty As of the time of writing, the type hierarchy of Tenet looks like this: -```@example kroki -mermaid"""graph TD +```mermaid +flowchart TD id1(AbstractTensorNetwork) id2(AbstractQuantum) id3(AbstractAnsatz) id4(AbstractMPO) id5(AbstractMPS) id1 -->|inherits| id2 -->|inherits| id3 -->|inherits| id4 -->|inherits| id5 - id1 -->|inherits| TensorNetwork - id2 -->|inherits| Quantum - id3 -->|inherits| Ansatz + id1 -->|implements| TensorNetwork + id2 -->|implements| Quantum + id3 -->|implements| Ansatz id3 -->|inherits| Product id4 -->|inherits| MPO id5 -->|inherits| MPS @@ -36,5 +32,4 @@ mermaid"""graph TD style id3 stroke-dasharray: 5 5 style id4 stroke-dasharray: 5 5 style id5 stroke-dasharray: 5 5 -""" ``` diff --git a/docs/src/manual/ansatz/mps.md b/docs/src/manual/ansatz/mps.md index 91190139e..4245bf838 100644 --- a/docs/src/manual/ansatz/mps.md +++ b/docs/src/manual/ansatz/mps.md @@ -1,33 +1,26 @@ # Matrix Product States (MPS) -Matrix Product States (MPS) are a Quantum Tensor Network ansatz whose tensors are laid out in a 1D chain. -Due to this, these networks are also known as _Tensor Trains_ in other mathematical fields. -Depending on the boundary conditions, the chains can be open or closed (i.e. periodic boundary conditions). - ```@setup viz using Makie -Makie.inline!(true) -set_theme!(resolution=(800,200)) - using CairoMakie - -using Tenet +using GraphMakie using NetworkLayout -``` - -```@example viz -fig = Figure() # hide - -tn_open = rand(MatrixProduct{State,Open}, n=10, χ=4) # hide -tn_periodic = rand(MatrixProduct{State,Periodic}, n=10, χ=4) # hide +using Tenet -plot!(fig[1,1], tn_open, layout=Spring(iterations=1000, C=0.5, seed=100)) # hide -plot!(fig[1,2], tn_periodic, layout=Spring(iterations=1000, C=0.5, seed=100)) # hide +# Page(offline=true) +# WGLMakie.activate!() +CairoMakie.activate!(type = "svg") +Makie.inline!(true) +set_theme!(resolution=(800,200)) +``` -Label(fig[1,1, Bottom()], "Open") # hide -Label(fig[1,2, Bottom()], "Periodic") # hide +Matrix Product States (MPS) are a Quantum Tensor Network ansatz whose tensors are laid out in a 1D chain. +Due to this, these networks are also known as _Tensor Trains_ in other mathematical fields. +Depending on the boundary conditions, the chains can be open or closed (i.e. periodic boundary conditions). -fig # hide +```@example viz +tn = rand(MPS; n=10, maxdim=2) # hide +graphplot(tn; layout=Stress()) # hide ``` ## Matrix Product Operators (MPO) @@ -36,18 +29,8 @@ Matrix Product Operators (MPO) are the operator version of [Matrix Product State The major difference between them is that MPOs have 2 indices per site (1 input and 1 output) while MPSs only have 1 index per site (i.e. an output). ```@example viz -fig = Figure() # hide - -tn_open = rand(MatrixProduct{Operator,Open}, n=10, χ=4) # hide -tn_periodic = rand(MatrixProduct{Operator,Periodic}, n=10, χ=4) # hide - -plot!(fig[1,1], tn_open, layout=Spring(iterations=1000, C=0.5, seed=100)) # hide -plot!(fig[1,2], tn_periodic, layout=Spring(iterations=1000, C=0.5, seed=100)) # hide - -Label(fig[1,1, Bottom()], "Open") # hide -Label(fig[1,2, Bottom()], "Periodic") # hide - -fig # hide +tn = rand(MPO, n=10, maxdim=2) # hide +graphplot(tn; layout=Stress()) # hide ``` In `Tenet`, the generic `MatrixProduct` ansatz implements this topology. Type variables are used to address their functionality (`State` or `Operator`) and their boundary conditions (`Open` or `Periodic`). diff --git a/docs/src/manual/transformations.md b/docs/src/manual/transformations.md index 5a2c97885..49cb16bb7 100644 --- a/docs/src/manual/transformations.md +++ b/docs/src/manual/transformations.md @@ -2,11 +2,14 @@ ```@setup plot using Makie -Makie.inline!(true) -using GraphMakie using CairoMakie -using Tenet +using GraphMakie using NetworkLayout +using Tenet + +CairoMakie.activate!(type = "svg") +Makie.inline!(true) +set_theme!(resolution=(800,200)) ``` In tensor network computations, it is good practice to apply various transformations to simplify the network structure, reduce computational cost, or prepare the network for further operations. These transformations modify the network's structure locally by permuting, contracting, factoring or truncating tensors. @@ -123,7 +126,7 @@ tn = TensorNetwork([ #hide reduced = transform(tn, Tenet.SplitSimplification) #hide graphplot!(fig[1, 1], tn; layout=Stress(), labels=true) #hide -graphplot!(fig[1, 2], reduced, layout=Spring(C=11); labels=true) #hide +graphplot!(fig[1, 2], reduced; layout=Spring(C=11), labels=true) #hide Label(fig[1, 1, Bottom()], "Original") #hide Label(fig[1, 2, Bottom()], "Transformed") #hide diff --git a/ext/TenetGraphMakieExt.jl b/ext/TenetGraphMakieExt.jl index a1b9e868d..879e75307 100644 --- a/ext/TenetGraphMakieExt.jl +++ b/ext/TenetGraphMakieExt.jl @@ -46,35 +46,7 @@ function GraphMakie.graphplot!(f::Union{Figure,GridPosition}, tn::Tenet.Abstract end function GraphMakie.graphplot!(ax::Union{Axis,Axis3}, tn::Tenet.AbstractTensorNetwork; labels=false, kwargs...) - tn = TensorNetwork(tn) - hypermap = Tenet.hyperflatten(tn) - tn = transform(tn, Tenet.HyperFlatten) - - tensormap = IdDict(tensor => i for (i, tensor) in enumerate(tensors(tn))) - - graph = Graphs.SimpleGraph(length(tensors(tn))) - for i in setdiff(inds(tn; set=:inner), inds(tn; set=:hyper)) - edge_tensors = tensors(tn; intersects=i) - - @assert length(edge_tensors) == 2 - a, b = edge_tensors - - Graphs.add_edge!(graph, tensormap[a], tensormap[b]) - end - - # TODO recognise `copytensors` by using `DeltaArray` or `Diagonal` representations - copytensors = findall(tensor -> any(flatinds -> issetequal(inds(tensor), flatinds), keys(hypermap)), tensors(tn)) - ghostnodes = map(inds(tn; set=:open)) do index - # create new ghost node - Graphs.add_vertex!(graph) - node = Graphs.nv(graph) - - # connect ghost node - tensor = only(tn.indexmap[index]) - Graphs.add_edge!(graph, node, tensormap[tensor]) - - return node - end + tn, graph, _, hypermap, copytensors, ghostnodes = Tenet.graph_representation(tn) # configure graphics # TODO refactor hardcoded values into constants diff --git a/src/Tenet.jl b/src/Tenet.jl index 820be50ad..8acdbab92 100644 --- a/src/Tenet.jl +++ b/src/Tenet.jl @@ -41,6 +41,8 @@ export MPS, MPO export evolve!, expect, overlap +include("Visualization.jl") + # reexports from EinExprs export einexpr, inds diff --git a/src/Transformations.jl b/src/Transformations.jl index f49c7fb88..93bea5c47 100644 --- a/src/Transformations.jl +++ b/src/Transformations.jl @@ -46,7 +46,7 @@ See also: [`HyperGroup`](@ref). """ struct HyperFlatten <: Transformation end -function hyperflatten(tn::TensorNetwork) +function hyperflatten(tn::AbstractTensorNetwork) return Dict( map(inds(tn; set=:hyper)) do hyperindex n = length(tensors(tn; intersects=hyperindex)) diff --git a/src/Visualization.jl b/src/Visualization.jl new file mode 100644 index 000000000..09711ccee --- /dev/null +++ b/src/Visualization.jl @@ -0,0 +1,196 @@ +using Graphs: Graphs, vertices, edges +using OrderedCollections: OrderedDict + +function graph_representation(tn::AbstractTensorNetwork) + tn = TensorNetwork(tn) + hypermap = Tenet.hyperflatten(tn) + if !isempty(hypermap) + tn = transform(tn, Tenet.HyperFlatten) + end + tensormap = IdDict(tensor => i for (i, tensor) in enumerate(tensors(tn))) + + graph = Graphs.SimpleGraph(length(tensors(tn))) + for i in setdiff(inds(tn; set=:inner), inds(tn; set=:hyper)) + a, b = tensors(tn; intersects=i) + Graphs.add_edge!(graph, tensormap[a], tensormap[b]) + end + + # TODO recognise `copytensors` by using `DeltaArray` or `Diagonal` representations + hypernodes = findall(tensor -> any(flatinds -> issetequal(inds(tensor), flatinds), keys(hypermap)), tensors(tn)) + ghostnodes = map(inds(tn; set=:open)) do index + # create new ghost node + Graphs.add_vertex!(graph) + node = Graphs.nv(graph) + + # connect ghost node + tensor = only(tn.indexmap[index]) + Graphs.add_edge!(graph, node, tensormap[tensor]) + + return node + end + + return tn, graph, tensormap, hypermap, hypernodes, ghostnodes +end + +# TODO use `Base.ENV["VSCODE_PID"]` to detect if running in vscode notebook? +# TODO move this to VSCodeServer.jl +function Base.Multimedia.istextmime(::MIME"application/vnd.vega.v5+json") + return true +end + +Base.show(io::IO, ::MIME"application/vnd.vega.v5+json", @nospecialize(tn::AbstractTensorNetwork)) = draw(io, tn) + +@inline draw(io::IO, args...) = draw(IOContext(io), args...) + +function draw(io::IOContext, @nospecialize(tn::AbstractTensorNetwork)) + width = get(io, :width, 800) + height = get(io, :height, 600) + + tn, graph, tensormap, hypermap, hypernodes, ghostnodes = graph_representation(tn) + hypermap = Dict(Iterators.flatten([[i => v for i in k] for (k, v) in hypermap])) + + spec = OrderedDict{Symbol,Any}() + spec[Symbol("\$schema")] = "https://vega.github.io/schema/vega/v5.json" + spec[:width] = width + spec[:height] = height + spec[:signals] = [ + OrderedDict(:name => "center.x", :update => "width / 2", :__collapse => true), + OrderedDict(:name => "center.y", :update => "height / 2", :__collapse => true), + ] + vdata = OrderedDict{Symbol,Any}() + edata = OrderedDict{Symbol,Any}() + spec[:data] = [vdata, edata] + vmarks = OrderedDict{Symbol,Any}() + emarks = OrderedDict{Symbol,Any}() + spec[:marks] = [vmarks, emarks] + + # vertex data + vdata[:name] = "vertex-data" + vdata[:format] = OrderedDict(:type => "json", :__collapse => true) + vdata[:values] = map(vertices(graph)) do v + group = if v ∈ ghostnodes + 0 + elseif v ∈ hypernodes + 1 + else + 2 + end + OrderedDict(:name => v, :group => group, :__collapse => true) + end + + # edge data + edata[:name] = "edge-data" + edata[:format] = OrderedDict(:type => "json", :__collapse => true) + edata[:values] = map(inds(tn)) do i + nodes = tensors(tn; intersects=i) + + if length(nodes) == 2 + # regular nodes + a, b = nodes + index = get(hypermap, i, i) + OrderedDict( + :source => tensormap[a] - 1, + :target => tensormap[b] - 1, + :value => string(index), + :__collapse => true, + ) + + elseif length(nodes) == 1 + # nodes with open indices + vertex = tensormap[only(nodes)] - 1 + ghost = ntensors(tn) + vertex + OrderedDict(:source => vertex, :target => ghost, :value => string(i), :__collapse => true) + end + end + + # vertex marks + vmarks[:name] = "vertices" + vmarks[:type] = "symbol" + vmarks[:from] = OrderedDict(:data => "vertex-data", :__collapse => true) + vmarks[:transform] = [ + OrderedDict( + :type => "force", + :static => get(io, :(var"force.static"), false), + :iterations => get(io, :(var"force.iterations"), 10000), + :forces => [ + OrderedDict( + :force => "center", + :x => OrderedDict(:signal => "center.x", :__collapse => true), + :y => OrderedDict(:signal => "center.y", :__collapse => true), + :__collapse => true, + ), + OrderedDict(:force => "collide", :radius => get(io, :(var"force.radius"), 1), :__collapse => true), + OrderedDict(:force => "nbody", :strength => get(io, :(var"force.strength"), -10), :__collapse => true), + OrderedDict( + :force => "link", + :links => "edge-data", + :distance => get(io, :(var"force.distance"), 10), + :__collapse => true, + ), + ], + ), + ] + + # edge marks + emarks[:type] = "path" + emarks[:from] = OrderedDict(:data => "edge-data", :__collapse => true) + emarks[:transform] = [ + OrderedDict( + :type => "linkpath", + :shape => "line", + :sourceX => "datum.source.x", + :sourceY => "datum.source.y", + :targetX => "datum.target.x", + :targetY => "datum.target.y", + ), + ] + emarks[:encode] = OrderedDict( + :update => OrderedDict( + :stroke => OrderedDict(:value => "#ccc", :__collapse => true), + :strokeWidth => OrderedDict(:value => get(io, :(var"edge.width"), 2), :__collapse => true), + :tooltip => OrderedDict(:field => "value", :__collapse => true), + ), + ) + + return print_json(io, spec) +end + +print_json(io::IOContext, spec::AbstractString) = print(io, "\"" * spec * "\"") +function print_json(io::IOContext, spec::AbstractDict) + indent = get(io, :indent, 0) + spec = copy(spec) + collapse = get(spec, :__collapse, false) + delete!(spec, :__collapse) + + print(io, '\t'^indent * "{" * (collapse ? "" : "\n")) + + for (i, (key, value)) in enumerate(spec) + print(io, (collapse ? "" : '\t'^(indent + 1)) * "\"$key\": ") + + if isa(value, AbstractDict) + print_json(IOContext(io, :indent => 0), value) + + elseif isa(value, Vector) + print(io, "[") + for (j, v) in enumerate(value) + print(io, collapse ? "" : "\n") + print_json(IOContext(io, :indent => indent + 2), v) + + islast = j == length(value) + print(io, (islast ? "" : ",")) + end + print(io, "\n" * '\t'^(indent + 1) * "]") + + elseif isa(value, AbstractString) + print(io, "\"$value\"") + + else + print(io, value) + end + + islast = i == length(spec) + print(io, (islast ? "" : ",") * (collapse ? "" : "\n")) + end + + print(io, (collapse ? "" : '\t'^indent) * "}") +end