Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,6 @@ result

# vm image
catcolab-vm.qcow2

# VSCode
.vscode/*
49 changes: 12 additions & 37 deletions packages/algjulia-interop/Project.toml
Original file line number Diff line number Diff line change
@@ -1,53 +1,28 @@
name = "CatColabInterop"
uuid = "9ecda8fb-39ab-46a2-a496-7285fa6368c1"
license = "MIT"
authors = ["CatColab team"]
version = "0.1.1"
authors = ["CatColab team"]

[deps]
ACSets = "227ef7b5-1206-438b-ac65-934d6da304b8"
Catlab = "134e5e36-593f-5add-ad60-77f754baafbe"
CombinatorialSpaces = "b1c52339-7909-45ad-8b6a-6e388f7c67f2"
ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66"
CoordRefSystems = "b46f11dc-f210-4604-bfba-323c1ec968cb"
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"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Oxygen = "df9a0d86-3283-4920-82dc-4555fc0d1d8b"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"

[weakdeps]
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
ACSets = "227ef7b5-1206-438b-ac65-934d6da304b8"
Catlab = "134e5e36-593f-5add-ad60-77f754baafbe"

[extensions]
SysImageExt = "PackageCompiler"
CatlabExt = ["Catlab", "ACSets"]

[compat]
ACSets = "0.2.21"
Catlab = "0.16.20"
CombinatorialSpaces = "0.7.4"
ComponentArrays = "0.15"
CoordRefSystems = "0.18.9"
Decapodes = "0.6"
DiagrammaticEquations = "0.2"
Distributions = "0.25"
GeometryBasics = "0.5.7"
IJulia = "1.26.0"
JSON3 = "1"
LinearAlgebra = "1"
MLStyle = "0.4"
OrdinaryDiffEq = "6.101.0"
PackageCompiler = "2.2.1"
Preferences = "1.5.0"
REPL = "1.11.0"
Catlab = "0.17.2"
HTTP = "1.10.19"
MLStyle = "0.4.17"
Oxygen = "1.7.5"
Reexport = "1.2.2"
StaticArrays = "1"
StructTypes = "1.11.0"
julia = "1.11"
120 changes: 120 additions & 0 deletions packages/algjulia-interop/ext/CatlabExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
module CatlabExt

using ACSets
using Catlab: Presentation, FreeSchema, Left
import Catlab: id, dom
using Catlab.CategoricalAlgebra.Pointwise.FunctorialDataMigrations.Yoneda:
yoneda, colimit_representables, DiagramData
using CatColabInterop, Oxygen, HTTP
import CatColabInterop: endpoint

"""
Take a parsed CatColab model of ThSchema and make a Catlab schema. Also
collect the mapping from UUIDs to human-readable names.
"""
function model_to_schema(m::Model)::Tuple{Schema, Dict{String,Symbol}}
obs, homs, attrtypes, attrs = Symbol[],[],[],[]
names = Dict{String, Symbol}()

for stmt in m.obGenerators
names[stmt.id] = Symbol(only(stmt.label))
if stmt.obType.content == "Entity"
push!(obs, names[stmt.id])
elseif stmt.obType.content == "AttrType"
push!(attrtypes, names[stmt.id])
else
error(stmt.obType)
end
end

for stmt in m.morGenerators
h = (Symbol(only(stmt.label)), names[stmt.dom.content],
names[stmt.cod.content])
names[stmt.id] = h[1]
if stmt.morType.content == "Attr"
push!(attrs, h)
else
push!(homs, h)
end
end
(Schema(Presentation(BasicSchema{Symbol}(obs, homs, attrtypes, attrs, []))),
names)
end

"""
Take a CatColab diagram in a model of ThSchema and construct the input data
that gets parsed normally from `@acset_colim`. Mutate an existing mapping of
UUIDs to schema-level names to include UUID mappings for instance-level names.
"""
function diagram_to_data(d::Types.Diagram, names::Dict{String,Symbol}
)::DiagramData
data = DiagramData()
for o in d.obGenerators
names[o.id] = Symbol(only(o.label))
push!(data.reprs[names[o.over.content]], names[o.id])
end
for m in d.morGenerators
p1 = names[m.cod.content] => Symbol[]
p2 = names[m.dom.content] => [names[m.over.content]]
push!(data.eqs, p1 => p2)
end
data
end

"""
Receiver of the data already knows the schema, so the JSON payload to CatColab
just includes the columns of data. Every part is named, so we use the names
(including for primary key columns) rather than numeric indices.
"""
function acset_to_json(X::ACSet, S::Schema, names::Dict{Symbol, Vector{String}}
)::AbstractDict
Dict{Symbol, Vector{String}}(
[t => names[t] for t in types(S)]
∪ [f => names[c][X[f]] for (f,_,c) in homs(S)]
∪ [f => names[c][getvalue.(X[f])] for (f,_,c) in attrs(S)] )
end

"""
Pick a human-readable name for all parts of the ACSet, given explicit names for
some of the parts. There is some ambiguity here (the vertex of a generic
reflexive edge `e` could be either `src(e)` or `tgt(e)`), but an arbitrary name
is chosen after minimizing length (`src(e)` preferred over `src(refl(src(e)))`).
"""
function make_names(res::ACSet, names::NamedTuple
)::Dict{Symbol, Vector{String}}
S = acset_schema(res)
function get_name(o::Symbol, i, curr=[])::Vector{Vector{Symbol}}
V(x) = o in attrtypes(S) ? AttrVar(x) : x # embellish attrvars
L(x) = o in attrtypes(S) ? Left(x) : x # embellish attrvars
found = findfirst(==((o, L(i))), names) # if (o,i) is in names
isnothing(found) || return [[found; curr]] # then just give the name
inc = [(d, new_i, f) for (f, d, _) in arrows(S, to=o)
for new_i in incident(res, V(i), f)]
return vcat([get_name(d, new_i, [f; curr]) for (d, new_i, f) in inc]...)
end
return Dict{Symbol, Vector{String}}(map(types(acset_schema(res))) do o
o => map(parts(res, o)) do i
possible_names = sort(get_name(o, i); by=length)
foldl((x,y)->"$y($x)", string.(first(possible_names)))
end
end)
end

"""
Top level function called by CatColab. Computes an ACSet colimit of a
diagrammatic instance. Return a JSON tabular representation.
"""
function endpoint(::Val{:ACSetColim})
@post "/acsetcolim" function(req::HTTP.Request)
payload = json(req, ModelDiagram)
schema, names = model_to_schema(payload.model)
data = diagram_to_data(payload.diagram, names)
acset_type = AnonACSet(
schema; type_assignment=Dict(a=>Nothing for a in schema.attrtypes))
y = yoneda(constructor(acset_type))
names, res = colimit_representables(data, y)
acset_to_json(res, schema, make_names(res, names))
end
end

end # module
24 changes: 0 additions & 24 deletions packages/algjulia-interop/ext/SysImageExt.jl

This file was deleted.

24 changes: 0 additions & 24 deletions packages/algjulia-interop/make_sysimage.jl

This file was deleted.

51 changes: 51 additions & 0 deletions packages/algjulia-interop/scripts/endpoint.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@

# Example usage:

# julia --project=my_alg_julia_env --threads 4 endpoint.jl Catlab AlgebraicPetri

# Where my_alg_julia_env is a Julia environment with CatColabInterop, Oxygen,
# HTTP, and any AlgJulia dependencies.

using CatColabInterop
using Oxygen
using HTTP

const CORS_HEADERS = [
"Access-Control-Allow-Origin" => "*",
"Access-Control-Allow-Headers" => "*",
"Access-Control-Allow-Methods" => "POST, GET, OPTIONS"
]

function CorsHandler(handle)
return function (req::HTTP.Request)
# return headers on OPTIONS request
if HTTP.method(req) == "OPTIONS"
return HTTP.Response(200, CORS_HEADERS)
else
r = handle(req)
append!(r.headers, ["Access-Control-Allow-Origin" => "*"])
r

end
end
end

defaults = [:Catlab,:ACSets] # all extensions to date

# Dynamically load packages in command lin eargs
for pkg in (isempty(ARGS) ? defaults : ARGS )
@info "using $pkg"
@eval using $pkg
end

for m in methods(CatColabInterop.endpoint)
sig = m.sig.parameters
(length(sig)==2 && sig[2].instance isa Val) || error("Unexpected signature $sig")
name = only(sig[2].parameters)
@info "Loading endpoint $name"
name isa Symbol || error("Unexpected endpoint name $name")
fntype, argtypes... = m.sig.types
invoke(fntype.instance, Tuple{argtypes...}, Val(name))
end

serve(middleware=[CorsHandler])
85 changes: 7 additions & 78 deletions packages/algjulia-interop/src/CatColabInterop.jl
Original file line number Diff line number Diff line change
@@ -1,86 +1,15 @@
module CatColabInterop

using MLStyle
using Reexport

# this code tracks integrations and allows for basic theory/model-building code to dispatch from it.
# the intent is that this is an interface for AlgebraicJulia code to interoperate with CatColab
abstract type AlgebraicJuliaIntegration end

# cells in the JSON are tagged. these are just objects for dispatching `to_model`
@data ModelElementTag begin
ObTag()
HomTag()
end
export ObTag, HomTag

#=
@active patterns are MLStyle-implementations of F# active patterns that forces us to work in the Maybe/Option pattern.
Practically, yet while a matter of opinion, they make @match statements cleaner; a statement amounts to a helpful pattern
name and the variables we intend to capture.
=#
@active IsObject(x) begin; x[:content][:tag] == "object" ? Some(x[:content]) : nothing end
@active IsMorphism(x) begin; x[:content][:tag] == "morphism" ? Some(x[:content]) : nothing end
export IsObject, IsMorphism

# Obs, Homs
@data ModelElementValue begin
ObValue()
HomValue(dom,cod)
end
export ObValue, HomValue

"""
Struct capturing the name of the object and its relevant information.
ModelElementValue may be objects or homs, each of which has different data.
"""
struct ModelElement
name::Union{Symbol, Nothing}
val::Union{<:ModelElementValue, Nothing}
function ModelElement(;name::Symbol=nothing,val::Any=nothing)
new(name, val)
end
end
export ModelElement

Base.nameof(t::ModelElement) = t.name
export endpoint

""" Struct wrapping a dictionary """
struct Model{T<:AlgebraicJuliaIntegration}
data::Dict{String, ModelElement}
end
export Model

function Model(::T) where T<:AlgebraicJuliaIntegration
Model{T}(Dict{String, ModelElement}())
end

Base.values(model::Model) = values(model.data)
using Reexport

"""
Functions to build a dictionary associating ids in the theory to elements in the model
Extend this method with endpoint(::Val{my_analysis_name}) in extension packages.
"""
function to_model end
export to_model


# TODO supposes bijection between theories, models, diagrams, etc.
abstract type AbstractDiagram{T<:AlgebraicJuliaIntegration} end

abstract type AbstractAnalysis{T<:AlgebraicJuliaIntegration} end

struct ImplError <: Exception
name::String
end
export ImplError

Base.showerror(io::IO, e::ImplError) = print(io, "$(e.name) not implemented")

include("result.jl")
include("kernel_management.jl")
include("kernel_support.jl")
include("decapodes-service/DecapodesService.jl")
function endpoint end

@reexport using .DecapodesService
include("Types.jl")
@reexport using .Types

end
end # module
Loading