diff --git a/packages/algjulia-service/.gitignore b/packages/algjulia-service/.gitignore new file mode 100644 index 000000000..29126e47b --- /dev/null +++ b/packages/algjulia-service/.gitignore @@ -0,0 +1,24 @@ +# Files generated by invoking Julia with --code-coverage +*.jl.cov +*.jl.*.cov + +# Files generated by invoking Julia with --track-allocation +*.jl.mem + +# System-specific files and directories generated by the BinaryProvider and BinDeps packages +# They contain absolute paths specific to the host computer, and so should not be committed +deps/deps.jl +deps/build.log +deps/downloads/ +deps/usr/ +deps/src/ + +# Build artifacts for creating documentation generated by the Documenter package +docs/build/ +docs/site/ + +# File generated by Pkg, the package manager, based on a corresponding Project.toml +# It records a fixed state of all packages used by the project. As such, it should not be +# committed for packages, but should be committed for applications that require a static +# environment. +Manifest.toml diff --git a/packages/algjulia-service/Project.toml b/packages/algjulia-service/Project.toml new file mode 100644 index 000000000..f991b4fec --- /dev/null +++ b/packages/algjulia-service/Project.toml @@ -0,0 +1,33 @@ +name = "AlgebraicJuliaService" +uuid = "9ecda8fb-39ab-46a2-a496-7285fa6368c1" +license = "MIT" +authors = ["CatColab team"] +version = "0.1.0" + +[deps] +ACSets = "227ef7b5-1206-438b-ac65-934d6da304b8" +CombinatorialSpaces = "b1c52339-7909-45ad-8b6a-6e388f7c67f2" +ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66" +Decapodes = "679ab3ea-c928-4fe6-8d59-fd451142d391" +DiagrammaticEquations = "6f00c28b-6bed-4403-80fa-30e0dc12f317" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[compat] +ACSets = "0.2.21" +ComponentArrays = "0.15" +Decapodes = "0.5.6" +DiagrammaticEquations = "0.1.7" +Distributions = "0.25" +GeometryBasics = "0.4" +JSON3 = "1" +LinearAlgebra = "1" +MLStyle = "0.4" +OrdinaryDiffEq = "6" +StaticArrays = "1" diff --git a/packages/algjulia-service/README.md b/packages/algjulia-service/README.md new file mode 100644 index 000000000..859f307de --- /dev/null +++ b/packages/algjulia-service/README.md @@ -0,0 +1,35 @@ +# AlgebraicJulia Service + +This small package makes functionality from +[AlgebraicJulia](https://www.algebraicjulia.org/) available to CatColab, +intermediated by a Julia kernel running in the [Jupyter](https://jupyter.org/) +server. At this time, only a +[Decapodes.jl](https://github.com/AlgebraicJulia/Decapodes.jl) service is +provided. Other packages may be added in the future. + +## Setup + +1. Install [Julia](https://julialang.org/), say by using +[`juliaup`](https://github.com/JuliaLang/juliaup) +2. Install [Jupyter](https://jupyter.org/), say by using `pip` or `conda` +3. Install [IJulia](https://github.com/JuliaLang/IJulia.jl), which provides the + Julia kernel to Jupyter + +At this stage, you should be able to launch a Julia kernel inside a JupyterLab. + +Having done that, navigate to this directory and run: + +```sh +julia --project -e 'import Pkg; Pkg.instantiate()' +``` + +## Usage + +Navigate to this directory and run: + +```sh +jupyter server --IdentityProvider.token="" --ServerApp.disable_check_xsrf=True --ServerApp.allow_origin="http://localhost:5173" +``` + +While the Jupyter server is running, the AlgebraicJulia service will be usable +by CatColab served locally. diff --git a/packages/algjulia-service/src/AlgebraicJuliaService.jl b/packages/algjulia-service/src/AlgebraicJuliaService.jl new file mode 100644 index 000000000..7a2b222cb --- /dev/null +++ b/packages/algjulia-service/src/AlgebraicJuliaService.jl @@ -0,0 +1,6 @@ +module AlgebraicJuliaService + +include("kernel_support.jl") +include("decapodes.jl") + +end diff --git a/packages/algjulia-service/src/decapodes.jl b/packages/algjulia-service/src/decapodes.jl new file mode 100644 index 000000000..a7a1aa564 --- /dev/null +++ b/packages/algjulia-service/src/decapodes.jl @@ -0,0 +1,312 @@ +# - parameterize by the theory. this is currently fixed to decapodes +# - switch to different meshes +# - use enum instead of val + +# algebraicjulia dependencies +using ACSets +using Decapodes +using DiagrammaticEquations +using CombinatorialSpaces + +# dependencies +import JSON3 +using StaticArrays +using MLStyle +using LinearAlgebra +using ComponentArrays +using Distributions # for initial conditions +using GeometryBasics: Point2, Point3 +using OrdinaryDiffEq + +export evalsim, default_dec_generate, DiagonalHodge, ComponentArray + +struct ImplError <: Exception + name::String +end + +Base.showerror(io::IO, e::ImplError) = print(io, "$(e.name) not implemented") + +function to_pode end +export to_pode + +""" Helper function to convert CatColab values (Obs) in Decapodes """ +function to_pode(::Val{:Ob}, name::String) + @match lowercase(name) begin + "0-form" => :Form0 + "1-form" => :Form1 + "2-form" => :Form2 + "dual 0-form" => :DualForm0 + "dual 1-form" => :DualForm1 + "dual 2-form" => :DualForm2 + "constant" => :Constant + x => throw(ImplError(x)) + end +end + +""" Helper function to convert CatColab values (Homs) in Decapodes """ +function to_pode(::Val{:Hom}, name::String) + @match name begin + "∂t" => :∂ₜ + "Δ" => :Δ + x => throw(ImplError(x)) + end +end + +# Build the theory + +# @active patterns are MLStyle-implementations of F# active patterns that forces us to work in the Maybe/Option design pattern. They make @match statements cleaner. +@active IsObject(x) begin + x[:tag] == "object" ? Some(x) : nothing +end + +@active IsMorphism(x) begin + x[:tag] == "morphism" ? Some(x) : nothing +end + +export IsObject, IsMorphism + +""" Obs, Homs """ +abstract type ElementData end + +""" Struct capturing the name of the object and its relevant information. ElementData may be objects or homs, each of which has different data. +""" +struct TheoryElement + name::Union{Symbol, Nothing} + val::Union{ElementData, Nothing} + function TheoryElement(;name::Symbol=nothing,val::Any=nothing) + new(name, val) + end +end +export TheoryElement + +Base.nameof(t::TheoryElement) = t.name + +struct HomData <: ElementData + dom::Any + cod::Any + function HomData(;dom::Any,cod::Any) + new(dom,cod) + end +end +export HomData + +struct Theory + data::Dict{String, TheoryElement} + function Theory() + new(Dict{String, TheoryElement}()) + end +end +export Theory + +# TODO engooden +Base.show(io::IO, theory::Theory) = println(io, theory.data) + +Base.values(theory::Theory) = values(theory.data) + +function add_to_theory! end +export add_to_theory! + +function add_to_theory!(theory::Theory, content::Any, type::Val{:Ob}) + push!(theory.data, content[:id] => TheoryElement(;name=to_pode(type, content[:name]))) +end + +function add_to_theory!(theory::Theory, content::Any, type::Val{:Hom}) + push!(theory.data, content[:id] => + TheoryElement(;name=to_pode(type, content[:name]), + val=HomData(dom=content[:dom][:content], + cod=content[:cod][:content]))) +end + +# for each cell, if it is... +# ...an object, we convert its type to a symbol and add it to the theorydict +# ...a morphism, we add it to the theorydict with a field for the ids of its +# domain and codomain to its +function Theory(model::AbstractVector{JSON3.Object}) + theory = Theory(); + foreach(model) do cell + @match cell begin + IsObject(content) => add_to_theory!(theory, content, Val(:Ob)) + IsMorphism(content) => add_to_theory!(theory, content, Val(:Hom)) + x => throw(ImplError(x)) + end + end + return theory +end +export Theory + +function add_to_pode! end +export add_to_pode! + +function add_to_pode!(d::SummationDecapode, + vars::Dict{String, Int}, # mapping between UUID and ACSet ID + theory::Theory, + content::JSON3.Object, + ::Val{:Ob}) + theory_elem = theory.data[content[:over][:content]] # indexes the theory by UUID + id = add_part!(d, :Var, name=Symbol(content[:name]), type=nameof(theory_elem)) + push!(vars, content[:id] => id) + d +end + +# TODO we are restricted to Op1 +function add_to_pode!(d::SummationDecapode, + vars::Dict{String, Int}, # mapping between UUID and ACSet ID + theory::Theory, + content::JSON3.Object, + ::Val{:Hom}) + dom = content[:dom][:content] + cod = content[:cod][:content] + if haskey(vars, dom) && haskey(vars, cod) + op1 = Symbol(theory.data[content[:over][:content]].name) + add_part!(d, :Op1, src=vars[dom], tgt=vars[cod], op1=op1) + # we need to add an inclusion to the TVar table + if op1 == :∂ₜ + add_part!(d, :TVar, incl=vars[cod]) + end + end + d +end + +""" Decapode(jsondiagram::JSON3.Object, theory::Theory) => SummationDecapode + +This returns a Decapode given a jsondiagram and a theory. +""" +function Decapode(diagram::AbstractVector{JSON3.Object}, theory::Theory) + # initiatize decapode and its mapping between UUIDs and ACSet IDs + pode = SummationDecapode(parse_decapode(quote end)) + vars = Dict{String, Int}(); + # for each cell in the notebook, add it to the diagram + foreach(diagram) do cell + @match cell begin + IsObject(content) => add_to_pode!(pode, vars, theory, content, Val(:Ob)) + IsMorphism(content) => add_to_pode!(pode, vars, theory, content, Val(:Hom)) + _ => throw(ImplError(cell[:content][:tag])) + end + end + pode +end +export Decapode +# the proper name for this constructor should be "SummationDecapode" + +function create_mesh() + s = triangulated_grid(100,100,2,2,Point2{Float64}) + sd = EmbeddedDeltaDualComplex2D{Bool, Float64, Point2{Float64}}(s) + subdivide_duals!(sd, Circumcenter()) + + # C = map(sd[:point]) do (x, _); return x end; + + c_dist = MvNormal([50, 50], [7.5, 7.5]) + c = [pdf(c_dist, [p[1], p[2]]) for p in sd[:point]] + u0 = ComponentArray(C=c) + + return (s, sd, u0, ()) +end +export create_mesh + +function run_sim(fm, u0, t0, constparam) + prob = ODEProblem(fm, u0, (0, t0), constparam) + soln = solve(prob, Tsit5(), saveat=0.1) +end +export run_sim + +abstract type AbstractPlotType end + +struct Heatmap <: AbstractPlotType end + +# TODO make length a conditional value so we can pass it in if we want +function Base.reshape(::Heatmap, data) + l = floor(Int64, sqrt(length(data))) + reshape(data, l, l) +end + +struct SimResult + time::Vector{Float64} + state::Vector{Matrix{SVector{3, Float64}}} + x::Vector{Float64} # axis + y::Vector{Float64} +end +export SimResult + +function SimResult(sol::ODESolution, mesh::EmbeddedDeltaDualComplex2D) + + points = collect(values(mesh.subparts.point.m)); + + function at_time(sol::ODESolution, timeidx::Int) + [SVector(i, j, sol.u[timeidx].C[51*(i-1) + j]) for i in 1:51, j in 1:51] + end + # TODO indexing by "C", more principled way of indexing (not hardcoding 51) + + state_vals = map(1:length(sol.t)) do i + at_time(sol, i) + end + + # TODO engooden + SimResult(sol.t, state_vals, 0:50, 0:50) +end +# TODO generalize to HasDeltaSet + +struct System + pode::SummationDecapode + mesh::HasDeltaSet + dualmesh::HasDeltaSet + init::Any # TODO specify. Is it always ComponentVector? +end +export System + +function System(json_string::String) + json_object = JSON3.read(json_string); + + # converts the JSON of (the fragment of) the theory + # into theory of the DEC, valued in Julia + theory = Theory(json_object[:model]) + + # this is a diagram in the model of the DEC. it wants to be a decapode! + diagram = json_object[:diagram] + + # pode + decapode = Decapode(diagram, theory); + + # mesh + s, sd, u0, _ = create_mesh(); + + return System(decapode, s, sd, u0) +end + +# TODO deprecated +function simulate_decapode(json_string::String) + + json_object = JSON3.read(json_string); + + # converts the JSON of (the fragment of) the theory + # into theory of the DEC, valued in Julia + theory = Theory(json_object[:model]) + + # this is a diagram in the model of the DEC. it wants to be a decapode! + diagram = json_object[:diagram] + + # pode + decapode = Decapode(diagram, theory); + + # mesh + s, sd, u0, _ = create_mesh(); + # TODO enhancement: generalize this + + # build simulation + simulator = evalsim(decapode); + f = simulator(sd, default_dec_generate, DiagonalHodge()); + # TODO enhancement: default_dec_generate could be generalized, maybe from the frontend + + # time + out = run_sim(f, u0, 10.0, ComponentArray(k=0.5,)); + # returns ::ODESolution + # - retcode + # - interpolation + # - t + # - u::Vector{ComponentVector} + + result = SimResult(out, sd); + + JsonValue(result) + +end +export simulate_decapode diff --git a/packages/algjulia-service/src/kernel_support.jl b/packages/algjulia-service/src/kernel_support.jl new file mode 100644 index 000000000..0b205898b --- /dev/null +++ b/packages/algjulia-service/src/kernel_support.jl @@ -0,0 +1,18 @@ +import JSON3 + +export JsonValue + +""" Container for an arbitrary JSON value. """ +struct JsonValue + value::Any +end + +function Base.show(io::IO, ::MIME"text/plain", json::JsonValue) + print(io, "JsonValue(") + show(IOContext(io, :compact => true), json.value) + print(io, ")") +end + +function Base.show(io::IO, ::MIME"application/json", json::JsonValue) + JSON3.write(io, json.value) +end diff --git a/packages/algjulia-service/src/test/Project.toml b/packages/algjulia-service/src/test/Project.toml new file mode 100644 index 000000000..a524cef15 --- /dev/null +++ b/packages/algjulia-service/src/test/Project.toml @@ -0,0 +1,2 @@ +[deps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" diff --git a/packages/algjulia-service/test/Project.toml b/packages/algjulia-service/test/Project.toml new file mode 100644 index 000000000..345c622e5 --- /dev/null +++ b/packages/algjulia-service/test/Project.toml @@ -0,0 +1,13 @@ +[deps] +ACSets = "227ef7b5-1206-438b-ac65-934d6da304b8" +AlgebraicJuliaService = "9ecda8fb-39ab-46a2-a496-7285fa6368c1" +CombinatorialSpaces = "b1c52339-7909-45ad-8b6a-6e388f7c67f2" +ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66" +Decapodes = "679ab3ea-c928-4fe6-8d59-fd451142d391" +DiagrammaticEquations = "6f00c28b-6bed-4403-80fa-30e0dc12f317" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/packages/algjulia-service/test/diffusion_data.json b/packages/algjulia-service/test/diffusion_data.json new file mode 100644 index 000000000..452b2435b --- /dev/null +++ b/packages/algjulia-service/test/diffusion_data.json @@ -0,0 +1,147 @@ +{ + "diagram": [ + { + "id": "01932402-bcf5-7432-8d14-dbae9eabf907", + "name": "C", + "obType": { + "content": "Object", + "tag": "Basic" + }, + "over": { + "content": "019323fa-49cb-7373-8c5d-c395bae4006d", + "tag": "Basic" + }, + "tag": "object" + }, + { + "id": "01932403-5b6c-7231-90d7-d7cece275eb2", + "name": "dC/dt", + "obType": { + "content": "Object", + "tag": "Basic" + }, + "over": { + "content": "019323fa-49cb-7373-8c5d-c395bae4006d", + "tag": "Basic" + }, + "tag": "object" + }, + { + "cod": { + "content": "01932403-5b6c-7231-90d7-d7cece275eb2", + "tag": "Basic" + }, + "dom": { + "content": "01932402-bcf5-7432-8d14-dbae9eabf907", + "tag": "Basic" + }, + "id": "01932403-c4cd-7563-8ebd-080dd37a9c7e", + "morType": { + "content": { + "content": "Object", + "tag": "Basic" + }, + "tag": "Hom" + }, + "name": "", + "over": { + "content": "019323fb-3652-7e91-aee9-06187a954fc6", + "tag": "Basic" + }, + "tag": "morphism" + }, + { + "cod": { + "content": "01932403-5b6c-7231-90d7-d7cece275eb2", + "tag": "Basic" + }, + "dom": { + "content": "01932402-bcf5-7432-8d14-dbae9eabf907", + "tag": "Basic" + }, + "id": "01932404-10e5-7128-bb94-835e5d8d643f", + "morType": { + "content": { + "content": "Object", + "tag": "Basic" + }, + "tag": "Hom" + }, + "name": "", + "over": { + "content": "019323ff-1af6-79da-b776-8ee11c88a8a0", + "tag": "Basic" + }, + "tag": "morphism" + } + ], + "model": [ + { + "id": "019323fa-49cb-7373-8c5d-c395bae4006d", + "name": "0-form", + "obType": { + "content": "Object", + "tag": "Basic" + }, + "tag": "object" + }, + { + "id": "019323fa-783b-72c8-af20-c0718fde3ac8", + "name": "1-form", + "obType": { + "content": "Object", + "tag": "Basic" + }, + "tag": "object" + }, + { + "id": "019323fb-175b-784e-aab8-7b78fa576571", + "name": "2-form", + "obType": { + "content": "Object", + "tag": "Basic" + }, + "tag": "object" + }, + { + "cod": { + "content": "019323fa-49cb-7373-8c5d-c395bae4006d", + "tag": "Basic" + }, + "dom": { + "content": "019323fa-49cb-7373-8c5d-c395bae4006d", + "tag": "Basic" + }, + "id": "019323fb-3652-7e91-aee9-06187a954fc6", + "morType": { + "content": { + "content": "Object", + "tag": "Basic" + }, + "tag": "Hom" + }, + "name": "∂t", + "tag": "morphism" + }, + { + "cod": { + "content": "019323fa-49cb-7373-8c5d-c395bae4006d", + "tag": "Basic" + }, + "dom": { + "content": "019323fa-49cb-7373-8c5d-c395bae4006d", + "tag": "Basic" + }, + "id": "019323ff-1af6-79da-b776-8ee11c88a8a0", + "morType": { + "content": { + "content": "Object", + "tag": "Basic" + }, + "tag": "Hom" + }, + "name": "Δ", + "tag": "morphism" + } + ] +} diff --git a/packages/algjulia-service/test/runtests.jl b/packages/algjulia-service/test/runtests.jl new file mode 100644 index 000000000..f18747529 --- /dev/null +++ b/packages/algjulia-service/test/runtests.jl @@ -0,0 +1,141 @@ +using Test + +using AlgebraicJuliaService +using ACSets +using CombinatorialSpaces +using Decapodes +using DiagrammaticEquations + +using MLStyle +using JSON3 +using ComponentArrays +using StaticArrays +import OrdinaryDiffEq: ReturnCode + +# visualization +using Plots + +# load data +data = open(JSON3.read, joinpath(@__DIR__, "diffusion_data.json"), "r") +diagram = data[:diagram]; +model = data[:model]; + +@testset "Text-to-Pode" begin + + @test to_pode(Val(:Ob), "0-form") == :Form0 + @test to_pode(Val(:Ob), "1-form") == :Form1 + @test to_pode(Val(:Ob), "2-form") == :Form2 + @test to_pode(Val(:Ob), "dual 0-form") == :DualForm0 + @test to_pode(Val(:Ob), "dual 1-form") == :DualForm1 + @test to_pode(Val(:Ob), "dual 2-form") == :DualForm2 + @test to_pode(Val(:Ob), "Constant") == :Constant + + @test_throws AlgebraicJuliaService.ImplError to_pode(Val(:Ob), "Form3") + + @test to_pode(Val(:Hom), "∂t") == :∂ₜ + @test to_pode(Val(:Hom), "Δ") == :Δ + @test_throws AlgebraicJuliaService.ImplError to_pode(Val(:Hom), "∧") + +end + +@testset "Parsing the Theory JSON Object" begin + + @test Set(keys(data)) == Set([:diagram, :model]) + + # the intent was to check that the JSON is coming in with the correct keys + # @test_broken Set(keys(model)) == Set([:name, :notebook, :theory, :type]) + + @test @match model[1] begin + IsObject(_) => true + _ => false + end + + @test @match model[4] begin + IsMorphism(_) => true + _ => false + end + + theory = Theory(); + @match model[1] begin + IsObject(content) => add_to_theory!(theory, content, Val(:Ob)) + _ => nothing + end + @test theory.data["019323fa-49cb-7373-8c5d-c395bae4006d"] == TheoryElement(;name=:Form0, val=nothing) + +end + +@testset "Making the Decapode" begin + + theory = Theory(model); + @test Set(nameof.(values(theory))) == Set([:Form0, :Form1, :Form2, :Δ, :∂ₜ]) + + handcrafted_pode = SummationDecapode(parse_decapode(quote end)); + add_part!(handcrafted_pode, :Var, name=:C, type=:Form0); + add_part!(handcrafted_pode, :Var, name=Symbol("dC/dt"), type=:Form0); + add_part!(handcrafted_pode, :TVar, incl=2); + add_part!(handcrafted_pode, :Op1, src=1, tgt=2, op1=:∂ₜ); + add_part!(handcrafted_pode, :Op1, src=1, tgt=2, op1=:Δ); + + decapode = Decapode(diagram, theory); + + @test decapode == handcrafted_pode + +end + +@testset "Simulation" begin + + json_string = read(joinpath(@__DIR__, "diffusion_data.json"), String); + system = System(json_string); + + simulator = evalsim(system.pode) + f = simulator(system.dualmesh, default_dec_generate, DiagonalHodge()); + + # time + soln = run_sim(f, system.init, 50.0, ComponentArray(k=0.5,)); + # returns ::ODESolution + # - retcode + # - interpolation + # - t + # - u::Vector{ComponentVector} + + @test soln.retcode == ReturnCode.Success + + result = SimResult(soln, system.dualmesh); + + @test typeof(result.state) == Vector{Matrix{SVector{3, Float64}}} + + jv = JsonValue(result); + +end + + +function save_fig(file, soln, mesh) + time = Observable(0.0) + fig = Figure() + Label(fig[1,1, Top()], @lift("...at $($time)"), padding = (0, 0, 5, 0)) + ax = CairoMakie.Axis(fig[1,1]) + msh = CairoMakie.mesh!(ax, mesh, + color=@lift(soln($time).C), + colormap=Reverse(:redsblues)) + Colorbar(fig[1,2], msh) + record(fig, file, soln.t[1:10:end]; framerate=10) do t + time[] = t + end +end +save_fig("testing_plot.mp4", soln, system.mesh) + +# TODO size fixed +function at_time(sr::SimResult, t::Int) + [sr.state[t][i,j][3] for i in 1:51 for j in 1:51] +end + +function show_heatmap(sr::SimResult, t::Int) + data = at_time(result, t) + ℓ = floor(Int64, sqrt(length(data))); + reshaped = reshape(data, ℓ, ℓ) + Plots.heatmap(1:51, 1:51, reshaped, clims=(minimum(data), maximum(data)); palette=:redsblues) +end + +@gif for t ∈ 1:length(result.time) + show_heatmap(result, t) +end every 5 diff --git a/packages/frontend/package.json b/packages/frontend/package.json index fe4e7bdd1..12daed229 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -27,6 +27,7 @@ "@corvu/disclosure": "^0.2.0", "@corvu/popover": "^0.2.0", "@corvu/resizable": "^0.2.3", + "@jupyterlab/services": "^7.3.1", "@kobalte/core": "^0.13.7", "@modular-forms/solid": "^0.24.1", "@qubit-rs/client": "^0.4.4", @@ -35,6 +36,7 @@ "@solid-primitives/context": "^0.2.3", "@solid-primitives/destructure": "^0.1.17", "@solid-primitives/keyboard": "^1.2.8", + "@solid-primitives/timer": "^1.3.10", "@solidjs/router": "^0.14.3", "@viz-js/viz": "^3.7.0", "catcolab-api": "link:../backend/pkg", diff --git a/packages/frontend/pnpm-lock.yaml b/packages/frontend/pnpm-lock.yaml index a8b1a5255..118de7b8c 100644 --- a/packages/frontend/pnpm-lock.yaml +++ b/packages/frontend/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@corvu/resizable': specifier: ^0.2.3 version: 0.2.3(solid-js@1.9.2) + '@jupyterlab/services': + specifier: ^7.3.1 + version: 7.3.1(react@18.3.1) '@kobalte/core': specifier: ^0.13.7 version: 0.13.7(solid-js@1.9.2) @@ -65,6 +68,9 @@ importers: '@solid-primitives/keyboard': specifier: ^1.2.8 version: 1.2.8(solid-js@1.9.2) + '@solid-primitives/timer': + specifier: ^1.3.10 + version: 1.3.10(solid-js@1.9.2) '@solidjs/router': specifier: ^0.14.3 version: 0.14.7(solid-js@1.9.2) @@ -929,6 +935,26 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@jupyter/ydoc@3.0.1': + resolution: {integrity: sha512-zO6PEe/kNpb9oRPhxytLdnL4zclVDJVCIEavprIlIs/qCxGbgC/BvIoK6N/Ny525Ljrev8Ku9B+y6P2qFn3zqg==} + + '@jupyterlab/coreutils@6.3.1': + resolution: {integrity: sha512-72Lyg2XA0Fsyn+62FpVIf67TGrxQZ4CNvldDvmlkixyY/n63bB38AcC0zvvCFDRv7QwXQzp7DaA0xfvAPoQ3bw==} + + '@jupyterlab/nbformat@4.3.1': + resolution: {integrity: sha512-9dIcagn5xQMKijPiu08MGAnfWQzk5mkqHRltHUfYbydV33lWgTjFBEpugHncEZGIxIJz+iqw2H9KJ8HSb7aJqw==} + + '@jupyterlab/services@7.3.1': + resolution: {integrity: sha512-ZXRW2wDV7kIoFRj8M3nkbaSxC6pYn4QlRtwJJEz6Ewges9x+EJEKoN7V0AVQ2zDG1XSmMvRLAvv1YvLgEk4Bug==} + + '@jupyterlab/settingregistry@4.3.1': + resolution: {integrity: sha512-tlRYh+CouyyDY3nGsCcD9hQKftdYKMwkTSV4X09vHlIEZoMNrbXtkyADgErgQGaIMwx7gtaLU4qCWK2LLMGyDw==} + peerDependencies: + react: '>=16' + + '@jupyterlab/statedb@4.3.1': + resolution: {integrity: sha512-Uph0kEPgCZvI6XnRsf7Md4WWA+4WnJMzwubc7cxk7B2KjdRYOqJHVtQbdJk4ZUA2ARhyS8tBtit/BZb9e3wLZw==} + '@kobalte/core@0.13.7': resolution: {integrity: sha512-COhjWk1KnCkl3qMJDvdrOsvpTlJ9gMLdemkAn5SWfbPn/lxJYabejnNOk+b/ILGg7apzQycgbuo48qb8ppqsAg==} peerDependencies: @@ -939,6 +965,36 @@ packages: peerDependencies: solid-js: ^1.8.8 + '@lumino/algorithm@2.0.2': + resolution: {integrity: sha512-cI8yJ2+QK1yM5ZRU3Kuaw9fJ/64JEDZEwWWp7+U0cd/mvcZ44BGdJJ29w+tIet1QXxPAvnsUleWyQ5qm4qUouA==} + + '@lumino/commands@2.3.1': + resolution: {integrity: sha512-DpX1kkE4PhILpvK1T4ZnaFb6UP4+YTkdZifvN3nbiomD64O2CTd+wcWIBpZMgy6MMgbVgrE8dzHxHk1EsKxNxw==} + + '@lumino/coreutils@2.2.0': + resolution: {integrity: sha512-x5wnQ/GjWBayJ6vXVaUi6+Q6ETDdcUiH9eSfpRZFbgMQyyM6pi6baKqJBK2CHkCc/YbAEl6ipApTgm3KOJ/I3g==} + + '@lumino/disposable@2.1.3': + resolution: {integrity: sha512-k5KXy/+T3UItiWHY4WwQawnsJnGo3aNtP5CTRKqo4+tbTNuhc3rTSvygJlNKIbEfIZXW2EWYnwfFDozkYx95eA==} + + '@lumino/domutils@2.0.2': + resolution: {integrity: sha512-2Kp6YHaMNI1rKB0PrALvOsZBHPy2EvVVAvJLWjlCm8MpWOVETjFp0MA9QpMubT9I76aKbaI5s1o1NJyZ8Y99pQ==} + + '@lumino/keyboard@2.0.2': + resolution: {integrity: sha512-icRUpvswDaFjqmAJNbQRb/aTu6Iugo6Y2oC08TiIwhQtLS9W+Ee9VofdqvbPSvCm6DkyP+DCWMuA3KXZ4V4g4g==} + + '@lumino/polling@2.1.3': + resolution: {integrity: sha512-WEZk96ddK6eHEhdDkFUAAA40EOLit86QVbqQqnbPmhdGwFogek26Kq9b1U273LJeirv95zXCATOJAkjRyb7D+w==} + + '@lumino/properties@2.0.2': + resolution: {integrity: sha512-b312oA3Bh97WFK8efXejYmC3DVJmvzJk72LQB7H3fXhfqS5jUWvL7MSnNmgcQvGzl9fIhDWDWjhtSTi0KGYYBg==} + + '@lumino/signaling@2.1.3': + resolution: {integrity: sha512-9Wd4iMk8F1i6pYjy65bqKuPlzQMicyL9xy1/ccS20kovPcfD074waneL/7BVe+3M8i+fGa3x2qjbWrBzOdTdNw==} + + '@lumino/virtualdom@2.0.2': + resolution: {integrity: sha512-HYZThOtZSoknjdXA102xpy5CiXtTFCVz45EXdWeYLx3NhuEwuAIX93QBBIhupalmtFlRg1yhdDNV40HxJ4kcXg==} + '@mdx-js/mdx@2.3.0': resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==} @@ -988,6 +1044,12 @@ packages: '@qubit-rs/client@0.4.4': resolution: {integrity: sha512-AdaF25aGRU4YkYYxxARH3eYlezzHhGECTFGZ5/zEc9y+iPxdIaaWf4/m8uEtdbzjMEsGaA4vVVd1kuriK6sDzQ==} + '@rjsf/utils@5.22.4': + resolution: {integrity: sha512-yQTdz5ryiYy258xCVthVPQ3DeaMzrRNrFcO8xvGHorp0/bLUxdTZ0iidXop49m3y8SaxxTZd398ZKWg2cqxiIA==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.14.0 || >=17 + '@rollup/plugin-virtual@3.0.2': resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} engines: {node: '>=14.0.0'} @@ -1165,6 +1227,11 @@ packages: peerDependencies: solid-js: ^1.6.12 + '@solid-primitives/timer@1.3.10': + resolution: {integrity: sha512-mCWUKjkw2oPlcT9SDjziDcz2qO4y6JXcSsmtAePKlfz6vUMIuL+Q+FK1NKUkpH+anMoVBGZLsSxI5P2+Y1RHNw==} + peerDependencies: + solid-js: ^1.6.12 + '@solid-primitives/trigger@1.1.0': resolution: {integrity: sha512-00BbAiXV66WwjHuKZc3wr0+GLb9C24mMUmi3JdTpNFgHBbrQGrIHubmZDg36c5/7wH+E0GQtOOanwQS063PO+A==} peerDependencies: @@ -1379,6 +1446,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1555,6 +1625,12 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + compute-gcd@1.2.1: + resolution: {integrity: sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==} + + compute-lcm@1.1.2: + resolution: {integrity: sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1683,9 +1759,15 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + faye-websocket@0.11.4: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} engines: {node: '>=0.8.0'} @@ -1888,6 +1970,9 @@ packages: peerDependencies: ws: '*' + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -1910,27 +1995,56 @@ packages: engines: {node: '>=6'} hasBin: true + json-schema-compare@0.2.2: + resolution: {integrity: sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==} + + json-schema-merge-allof@0.8.1: + resolution: {integrity: sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==} + engines: {node: '>=12.0.0'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + lib0@0.2.98: + resolution: {integrity: sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==} + engines: {node: '>=16'} + hasBin: true + linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + loupe@3.1.1: resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} @@ -2108,6 +2222,9 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@3.3.6: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} @@ -2178,6 +2295,9 @@ packages: parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -2290,12 +2410,19 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + raf-schd@4.0.3: resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -2322,6 +2449,13 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -2618,6 +2752,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2652,6 +2789,21 @@ packages: validate-html-nesting@1.2.2: resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} + validate.io-array@1.0.6: + resolution: {integrity: sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==} + + validate.io-function@1.0.2: + resolution: {integrity: sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==} + + validate.io-integer-array@1.0.0: + resolution: {integrity: sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==} + + validate.io-integer@1.0.5: + resolution: {integrity: sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==} + + validate.io-number@1.0.3: + resolution: {integrity: sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -2816,6 +2968,12 @@ packages: xstate@5.18.2: resolution: {integrity: sha512-hab5VOe29D0agy8/7dH1lGw+7kilRQyXwpaChoMu4fe6rDP+nsHYhDYKfS2O4iXE7myA98TW6qMEudj/8NXEkA==} + y-protocols@1.0.6: + resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -2839,6 +2997,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yjs@13.6.20: + resolution: {integrity: sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -3676,6 +3838,67 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jupyter/ydoc@3.0.1': + dependencies: + '@jupyterlab/nbformat': 4.3.1 + '@lumino/coreutils': 2.2.0 + '@lumino/disposable': 2.1.3 + '@lumino/signaling': 2.1.3 + y-protocols: 1.0.6(yjs@13.6.20) + yjs: 13.6.20 + + '@jupyterlab/coreutils@6.3.1': + dependencies: + '@lumino/coreutils': 2.2.0 + '@lumino/disposable': 2.1.3 + '@lumino/signaling': 2.1.3 + minimist: 1.2.8 + path-browserify: 1.0.1 + url-parse: 1.5.10 + + '@jupyterlab/nbformat@4.3.1': + dependencies: + '@lumino/coreutils': 2.2.0 + + '@jupyterlab/services@7.3.1(react@18.3.1)': + dependencies: + '@jupyter/ydoc': 3.0.1 + '@jupyterlab/coreutils': 6.3.1 + '@jupyterlab/nbformat': 4.3.1 + '@jupyterlab/settingregistry': 4.3.1(react@18.3.1) + '@jupyterlab/statedb': 4.3.1 + '@lumino/coreutils': 2.2.0 + '@lumino/disposable': 2.1.3 + '@lumino/polling': 2.1.3 + '@lumino/properties': 2.0.2 + '@lumino/signaling': 2.1.3 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - react + - utf-8-validate + + '@jupyterlab/settingregistry@4.3.1(react@18.3.1)': + dependencies: + '@jupyterlab/nbformat': 4.3.1 + '@jupyterlab/statedb': 4.3.1 + '@lumino/commands': 2.3.1 + '@lumino/coreutils': 2.2.0 + '@lumino/disposable': 2.1.3 + '@lumino/signaling': 2.1.3 + '@rjsf/utils': 5.22.4(react@18.3.1) + ajv: 8.17.1 + json5: 2.2.3 + react: 18.3.1 + + '@jupyterlab/statedb@4.3.1': + dependencies: + '@lumino/commands': 2.3.1 + '@lumino/coreutils': 2.2.0 + '@lumino/disposable': 2.1.3 + '@lumino/properties': 2.0.2 + '@lumino/signaling': 2.1.3 + '@kobalte/core@0.13.7(solid-js@1.9.2)': dependencies: '@floating-ui/dom': 1.6.11 @@ -3699,6 +3922,47 @@ snapshots: '@solid-primitives/utils': 6.2.3(solid-js@1.9.2) solid-js: 1.9.2 + '@lumino/algorithm@2.0.2': {} + + '@lumino/commands@2.3.1': + dependencies: + '@lumino/algorithm': 2.0.2 + '@lumino/coreutils': 2.2.0 + '@lumino/disposable': 2.1.3 + '@lumino/domutils': 2.0.2 + '@lumino/keyboard': 2.0.2 + '@lumino/signaling': 2.1.3 + '@lumino/virtualdom': 2.0.2 + + '@lumino/coreutils@2.2.0': + dependencies: + '@lumino/algorithm': 2.0.2 + + '@lumino/disposable@2.1.3': + dependencies: + '@lumino/signaling': 2.1.3 + + '@lumino/domutils@2.0.2': {} + + '@lumino/keyboard@2.0.2': {} + + '@lumino/polling@2.1.3': + dependencies: + '@lumino/coreutils': 2.2.0 + '@lumino/disposable': 2.1.3 + '@lumino/signaling': 2.1.3 + + '@lumino/properties@2.0.2': {} + + '@lumino/signaling@2.1.3': + dependencies: + '@lumino/algorithm': 2.0.2 + '@lumino/coreutils': 2.2.0 + + '@lumino/virtualdom@2.0.2': + dependencies: + '@lumino/algorithm': 2.0.2 + '@mdx-js/mdx@2.3.0': dependencies: '@types/estree-jsx': 1.0.5 @@ -3758,6 +4022,15 @@ snapshots: '@qubit-rs/client@0.4.4': {} + '@rjsf/utils@5.22.4(react@18.3.1)': + dependencies: + json-schema-merge-allof: 0.8.1 + jsonpointer: 5.0.1 + lodash: 4.17.21 + lodash-es: 4.17.21 + react: 18.3.1 + react-is: 18.3.1 + '@rollup/plugin-virtual@3.0.2(rollup@4.22.5)': optionalDependencies: rollup: 4.22.5 @@ -3916,6 +4189,10 @@ snapshots: '@solid-primitives/utils': 6.2.3(solid-js@1.9.2) solid-js: 1.9.2 + '@solid-primitives/timer@1.3.10(solid-js@1.9.2)': + dependencies: + solid-js: 1.9.2 + '@solid-primitives/trigger@1.1.0(solid-js@1.9.2)': dependencies: '@solid-primitives/utils': 6.2.3(solid-js@1.9.2) @@ -4122,6 +4399,13 @@ snapshots: acorn@8.12.1: {} + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -4309,6 +4593,19 @@ snapshots: comma-separated-tokens@2.0.3: {} + compute-gcd@1.2.1: + dependencies: + validate.io-array: 1.0.6 + validate.io-function: 1.0.2 + validate.io-integer-array: 1.0.0 + + compute-lcm@1.1.2: + dependencies: + compute-gcd: 1.2.1 + validate.io-array: 1.0.6 + validate.io-function: 1.0.2 + validate.io-integer-array: 1.0.0 + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -4458,8 +4755,12 @@ snapshots: extend@3.0.2: {} + fast-deep-equal@3.1.3: {} + fast-sha256@1.3.0: {} + fast-uri@3.0.3: {} + faye-websocket@0.11.4: dependencies: websocket-driver: 0.7.4 @@ -4718,6 +5019,8 @@ snapshots: dependencies: ws: 8.18.0 + isomorphic.js@0.2.5: {} + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -4739,20 +5042,46 @@ snapshots: jsesc@3.0.2: {} + json-schema-compare@0.2.2: + dependencies: + lodash: 4.17.21 + + json-schema-merge-allof@0.8.1: + dependencies: + compute-lcm: 1.1.2 + json-schema-compare: 0.2.2 + lodash: 4.17.21 + + json-schema-traverse@1.0.0: {} + json5@2.2.3: {} + jsonpointer@5.0.1: {} + kleur@4.1.5: {} + lib0@0.2.98: + dependencies: + isomorphic.js: 0.2.5 + linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 + lodash-es@4.17.21: {} + lodash.camelcase@4.3.0: {} + lodash@4.17.21: {} + long@5.2.3: {} longest-streak@3.1.0: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + loupe@3.1.1: dependencies: get-func-name: 2.0.2 @@ -5136,6 +5465,8 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimist@1.2.8: {} + minipass@3.3.6: dependencies: yallist: 4.0.0 @@ -5197,6 +5528,8 @@ snapshots: dependencies: entities: 4.5.0 + path-browserify@1.0.1: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -5333,10 +5666,16 @@ snapshots: punycode.js@2.3.1: {} + querystringify@2.2.0: {} + raf-schd@4.0.3: {} react-is@18.3.1: {} + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -5375,6 +5714,10 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + resolve@1.22.8: dependencies: is-core-module: 2.15.1 @@ -5717,6 +6060,11 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.0 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + util-deprecate@1.0.2: {} uuid@10.0.0: {} @@ -5740,6 +6088,21 @@ snapshots: validate-html-nesting@1.2.2: {} + validate.io-array@1.0.6: {} + + validate.io-function@1.0.2: {} + + validate.io-integer-array@1.0.0: + dependencies: + validate.io-array: 1.0.6 + validate.io-integer: 1.0.5 + + validate.io-integer@1.0.5: + dependencies: + validate.io-number: 1.0.3 + + validate.io-number@1.0.3: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -5915,6 +6278,11 @@ snapshots: xstate@5.18.2: {} + y-protocols@1.0.6(yjs@13.6.20): + dependencies: + lib0: 0.2.98 + yjs: 13.6.20 + y18n@5.0.8: {} yallist@3.1.1: {} @@ -5935,6 +6303,10 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yjs@13.6.20: + dependencies: + lib0: 0.2.98 + yn@3.1.1: {} zrender@5.6.0: diff --git a/packages/frontend/src/stdlib/analyses/decapodes.tsx b/packages/frontend/src/stdlib/analyses/decapodes.tsx new file mode 100644 index 000000000..b25bb7414 --- /dev/null +++ b/packages/frontend/src/stdlib/analyses/decapodes.tsx @@ -0,0 +1,162 @@ +import { Match, Switch, createResource, onCleanup } from "solid-js"; + +import type { DiagramAnalysisProps } from "../../analysis"; +import { ErrorAlert, IconButton, Warning } from "../../components"; +import type { DiagramAnalysisMeta } from "../../theory"; +import { PDEPlot2D, type PDEPlotData2D } from "../../visualization"; + +import Loader from "lucide-solid/icons/loader"; +import RotateCcw from "lucide-solid/icons/rotate-ccw"; + +import baseStyles from "./base_styles.module.css"; +import "./simulation.css"; + +type JupyterSettings = { + baseUrl?: string; + token?: string; +}; + +export function configureDecapodes(options: { + id?: string; + name?: string; + description?: string; +}): DiagramAnalysisMeta { + const { + id = "decapodes", + name = "Simulation", + description = "Simulate the PDE using Decapodes", + } = options; + return { + id, + name, + description, + component: (props) => , + initialContent: () => ({}), + }; +} + +export function Decapodes(props: DiagramAnalysisProps) { + const [kernel, { refetch: restartKernel }] = createResource(async () => { + const jupyter = await import("@jupyterlab/services"); + + const serverSettings = jupyter.ServerConnection.makeSettings({ + baseUrl: props.content.baseUrl ?? "http://127.0.0.1:8888", + token: props.content.token ?? "", + }); + + const kernelManager = new jupyter.KernelManager({ serverSettings }); + const kernel = await kernelManager.startNew({ name: "julia-1.11" }); + + const future = kernel.requestExecute({ code: initJuliaCode }); + const reply = await future.done; + + if (reply.content.status === "error") { + await kernel.shutdown(); + throw new Error(reply.content.evalue); + } + + return kernel; + }); + + onCleanup(() => kernel()?.shutdown()); + + const maybeKernel = () => (kernel.error ? undefined : kernel()); + + const [result, { refetch: rerunSimulation }] = createResource(maybeKernel, async (kernel) => { + const simulationData = { + diagram: props.liveDiagram.formalJudgments(), + model: props.liveDiagram.liveModel.formalJudgments(), + }; + const future = kernel.requestExecute({ + code: makeJuliaSimulationCode(simulationData), + }); + + let result: PDEPlotData2D | undefined; + future.onIOPub = (msg) => { + if ( + msg.header.msg_type === "execute_result" && + msg.parent_header.msg_id === future.msg.header.msg_id + ) { + const content = msg.content as JsonDataContent; + result = content["data"]?.["application/json"]; + } + }; + + const reply = await future.done; + if (reply.content.status === "error") { + throw new Error(reply.content.evalue); + } + if (!result) { + throw new Error("Result not received from the simulator"); + } + return result; + }); + + return ( +
+
+ Simulation + + + + + + + + + + + + + + + + + + +
+ + {"Loading the AlgebraicJulia service..."} + + {(error) => ( + + {error().message} + + )} + + {"Running the simulation..."} + + {(error) => {error().message}} + + {(data) => } + +
+ ); +} + +type JsonDataContent = { + data?: { + "application/json"?: T; + }; +}; + +const initJuliaCode = ` +import IJulia +IJulia.register_jsonmime(MIME"application/json"()) + +using AlgebraicJuliaService +`; + +const makeJuliaSimulationCode = (data: unknown) => ` +system = System(raw"""${JSON.stringify(data)}"""); + +simulator = evalsim(system.pode); +f = simulator(system.dualmesh, default_dec_generate, DiagonalHodge()); + +soln = run_sim(f, system.init, 100.0, ComponentArray(k=0.5,)); + +JsonValue(SimResult(soln, system.dualmesh)) +`; diff --git a/packages/frontend/src/stdlib/analyses/index.ts b/packages/frontend/src/stdlib/analyses/index.ts index 77e462541..5cca632f3 100644 --- a/packages/frontend/src/stdlib/analyses/index.ts +++ b/packages/frontend/src/stdlib/analyses/index.ts @@ -1,5 +1,6 @@ +export * from "./decapodes"; +export * from "./diagram_graph"; export * from "./lotka_volterra"; export * from "./model_graph"; -export * from "./diagram_graph"; export * from "./stock_flow_diagram"; export * from "./submodel_graphs"; diff --git a/packages/frontend/src/stdlib/theories.tsx b/packages/frontend/src/stdlib/theories.tsx index 7246ac88c..91bbd4e7b 100644 --- a/packages/frontend/src/stdlib/theories.tsx +++ b/packages/frontend/src/stdlib/theories.tsx @@ -2,6 +2,7 @@ import * as catlog from "catlog-wasm"; import { Theory } from "../theory"; import { + configureDecapodes, configureDiagramGraph, configureLotkaVolterra, configureModelGraph, @@ -509,6 +510,7 @@ stdTheories.add( name: "Diagram", description: "Visualize the equations as a diagram", }), + configureDecapodes({}), ], }); }, diff --git a/packages/frontend/src/visualization/index.ts b/packages/frontend/src/visualization/index.ts index 653c60b33..08d553407 100644 --- a/packages/frontend/src/visualization/index.ts +++ b/packages/frontend/src/visualization/index.ts @@ -12,6 +12,7 @@ export * from "./graph_svg"; export * from "./graphviz"; export * from "./graphviz_svg"; export * from "./ode_plot"; +export * from "./pde_plot"; export type * as GraphLayout from "./graph_layout"; export type * as GraphvizJSON from "./graphviz_json"; diff --git a/packages/frontend/src/visualization/ode_plot.tsx b/packages/frontend/src/visualization/ode_plot.tsx index e41838549..f8a01d438 100644 --- a/packages/frontend/src/visualization/ode_plot.tsx +++ b/packages/frontend/src/visualization/ode_plot.tsx @@ -6,18 +6,18 @@ import { ErrorAlert } from "../components"; const ECharts = lazy(() => import("./echarts")); -/** Values of a state variable over time. */ -export type StateVarData = { - name: string; - data: number[]; -}; - /** Data plotted by `ODEPlot` component. */ export type ODEPlotData = { time: number[]; states: StateVarData[]; }; +/** Values of a state variable over time. */ +type StateVarData = { + name: string; + data: number[]; +}; + /** Display the results from an ODE simulation. Plots the output data if the simulation was successful and shows an error @@ -29,11 +29,7 @@ export function ODEResultPlot(props: { return ( - {(data) => ( -
- -
- )} + {(data) => }
{(err) => {err()}} @@ -71,7 +67,11 @@ export function ODEPlot(props: { })), }); - return ; + return ( +
+ +
+ ); } const formatTimeLabel = (x: number): string => { diff --git a/packages/frontend/src/visualization/pde_plot.tsx b/packages/frontend/src/visualization/pde_plot.tsx new file mode 100644 index 000000000..71de62936 --- /dev/null +++ b/packages/frontend/src/visualization/pde_plot.tsx @@ -0,0 +1,108 @@ +import { makeTimer } from "@solid-primitives/timer"; +import type { EChartsOption } from "echarts"; +import { createMemo, createSignal, lazy } from "solid-js"; + +const ECharts = lazy(() => import("./echarts")); + +/** Data plotted by `PDEPlot2D` component. */ +export type PDEPlotData2D = { + /** Time values. */ + time: number[]; + + /** Values of x-coordinate. */ + x: number[]; + + /** Values of y-coordinate. */ + y: number[]; + + /** Values of the state variable over time. */ + state: StateVarAtTime[]; +}; + +/** The data of a state variable at a given time. */ +type StateVarAtTime = Array<[xIndex: number, yIndex: number, value: number]>; + +/** Display the output data from a 2D PDE simulation. */ +export function PDEPlot2D(props: { + data: PDEPlotData2D; +}) { + // XXX: JavaScript is not stable under eta-equivalence. + const min = (x: number, y: number) => Math.min(x, y); + const max = (x: number, y: number) => Math.max(x, y); + + const minValue = createMemo(() => + props.data.state.map((data) => data.map((triple) => triple[2]).reduce(min)).reduce(min), + ); + const maxValue = createMemo(() => + props.data.state.map((data) => data.map((triple) => triple[2]).reduce(max)).reduce(max), + ); + + const [timeIndex, setTimeIndex] = createSignal(0); + + // Animate the heat map by varying the time index to display. + makeTimer( + () => { + const timeLength = props.data.time.length; + setTimeIndex((timeIndex() + 5) % timeLength); + }, + 10, + setInterval, + ); + + function options(idx: number): EChartsOption { + return { + xAxis: { + type: "category", + data: props.data.x, + }, + yAxis: { + type: "category", + data: props.data.y, + }, + visualMap: { + min: minValue(), + max: maxValue(), + orient: "horizontal", + left: "center", + inRange: { + // Source for colors: + // https://echarts.apache.org/examples/en/editor.html?c=heatmap-large + color: [ + "#313695", + "#4575b4", + "#74add1", + "#abd9e9", + "#e0f3f8", + "#ffffbf", + "#fee090", + "#fdae61", + "#f46d43", + "#d73027", + "#a50026", + ], + }, + }, + series: [ + { + name: "Value", + type: "heatmap", + data: props.data.state[idx], + emphasis: { + itemStyle: { + borderColor: "black", + borderWidth: 1, + }, + }, + progressive: false, + animation: false, + }, + ], + }; + } + + return ( +
+ +
+ ); +}