Skip to content

Commit 487862b

Browse files
committed
feat: add constructor for QN via BMA JSON files
Add functionality to parse BMA's JSON format to a QN. To access this, you must be `using JSON`.
1 parent cfeb0b1 commit 487862b

File tree

4 files changed

+171
-0
lines changed

4 files changed

+171
-0
lines changed

Project.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1818
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
1919
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
2020

21+
[weakdeps]
22+
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
23+
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
24+
25+
[extensions]
26+
JSONExt = ["JSON", "MacroTools"]
27+
2128
[compat]
2229
AbstractTrees = "0.4.5"
2330
DocStringExtensions = "0.9.3"

ext/JSONExt.jl

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
module JSONExt
2+
import JSON
3+
4+
using GraphDynamicalSystems: Asynchronous, QualitativeNetwork, default_target_function
5+
using Graphs: SimpleDiGraph, add_edge!, add_vertex!
6+
using MacroTools: @capture, postwalk
7+
using MetaGraphsNext: MetaGraph, inneighbor_labels
8+
9+
function nested_dicts_keys_to_lowercase(d)
10+
if d isa AbstractDict
11+
return Dict([lowercase(k) => nested_dicts_keys_to_lowercase(v) for (k, v) in d])
12+
elseif d isa AbstractVector
13+
return [nested_dicts_keys_to_lowercase(v) for v in d]
14+
else
15+
return d
16+
end
17+
end
18+
19+
function sanitize_formula(f)
20+
# surround variable names with quotes
21+
return replace(f, r"var\(([^\)]+)\)" => s"var(\"\1\")")
22+
end
23+
24+
function entity_name_from_in_neighbors(entity, in_neighbors)
25+
# the formulas can reference their incoming edges
26+
# with either the name of the neighbor entity or
27+
# its id
28+
e_id = tryparse(Int, entity)
29+
30+
entity_name = [
31+
Symbol("$(name)_$id") for
32+
(id, name, _) in in_neighbors if isnothing(e_id) ? name == entity : id == e_id
33+
]
34+
35+
if length(entity_name) != 1
36+
error(
37+
"Error while constructing name for entity: $entity, with in neighbors: $in_neighbors",
38+
)
39+
end
40+
return only(entity_name)
41+
end
42+
43+
function create_target_function(
44+
variable::Dict,
45+
in_neighbor_ids::Vector{Int},
46+
id_to_name::Dict,
47+
mg::MetaGraph,
48+
)
49+
formula = Meta.parse(sanitize_formula(variable["formula"]))
50+
in_neighbor_names = getindex.((id_to_name,), in_neighbor_ids)
51+
in_neighbor_types = getindex.((mg.edge_data,), in_neighbor_ids, (variable["id"],))
52+
in_neighbors = zip(in_neighbor_ids, in_neighbor_names, in_neighbor_types)
53+
54+
if isnothing(formula) # default target function
55+
if length(in_neighbor_ids) == 0
56+
@warn "$(variable["name"]) has no inputs, defaulting formula to lowest value ($(variable["rangefrom"]))."
57+
return variable["rangefrom"]
58+
else
59+
activators = [
60+
Symbol("$(name)_$id") for
61+
(id, name, ty) in in_neighbors if ty == "Activator"
62+
]
63+
inhibitors = [
64+
Symbol("$(name)_$id") for
65+
(id, name, ty) in in_neighbors if ty == "Inhibitor"
66+
]
67+
return default_target_function(
68+
variable["rangefrom"],
69+
variable["rangeto"],
70+
activators,
71+
inhibitors,
72+
)
73+
end
74+
else # custom target function
75+
return postwalk(
76+
x ->
77+
@capture(x, var(v_String)) ?
78+
:($(entity_name_from_in_neighbors(v, in_neighbors))) : x,
79+
formula,
80+
)
81+
end
82+
end
83+
84+
function to_from_variable_id(r, from_to)
85+
k = "$(from_to)variable"
86+
k_w_id = k * "id"
87+
88+
if haskey(r, k)
89+
return r[k]
90+
elseif haskey(r, k_w_id)
91+
return r[k_w_id]
92+
else
93+
@show keys(r)
94+
error("""
95+
Neither alternative key was found to retrieve the edge variable id. The \
96+
model file is not using the expected structure for BMA models.
97+
""")
98+
end
99+
end
100+
101+
function QualitativeNetwork(bma_file_path::AbstractString)
102+
json_def = JSON.parse(read(bma_file_path, String))
103+
104+
json_def = nested_dicts_keys_to_lowercase(json_def)
105+
model = json_def["model"]
106+
variables = model["variables"]
107+
relationships = model["relationships"]
108+
109+
id_to_name = Dict([v["id"] => v["name"] for v in variables])
110+
names = [Symbol("$(v["name"])_$(v["id"])") for v in variables]
111+
mg = MetaGraph(SimpleDiGraph(), Int, Union{Expr,Integer,Symbol}, String)
112+
113+
foreach(id_to_name) do (id, n)
114+
# adding an empty expression: :()
115+
# because we need to construct the interaction graph
116+
# first before parsing the functions correctly
117+
added = add_vertex!(mg, id, :())
118+
if !added
119+
error(
120+
"""
121+
Failed to add the entity ($n, id: $id) from the input file while \
122+
constructing the QN. Check that there is only one entity in the model with \
123+
the id $id.
124+
""",
125+
)
126+
end
127+
end
128+
129+
foreach(relationships) do r
130+
from = to_from_variable_id(r, "from")
131+
to = to_from_variable_id(r, "to")
132+
type_of_edge = r["type"]
133+
added = add_edge!(mg, from, to, type_of_edge)
134+
if !added
135+
@warn """
136+
Encountered a duplicate relationship between entities (from: \
137+
$(id_to_name[from]), #$from; to: $(id_to_name[to]), #$to) while constructing \
138+
the QN.
139+
"""
140+
end
141+
end
142+
143+
formulas = Union{Expr,Integer,Symbol}[
144+
create_target_function(v, collect(inneighbor_labels(mg, v["id"])), id_to_name, mg) for v in variables
145+
]
146+
147+
# @show formulas
148+
# formulas = Union{Expr,Integer,Symbol}[v["formula"] for v in variables]
149+
domains = [v["rangefrom"]:v["rangeto"] for v in variables]
150+
#
151+
return QualitativeNetwork(names, formulas, domains; schedule = Asynchronous)
152+
end
153+
end

test/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4"
55
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
66
HerbCore = "2b23ba43-8213-43cb-b5ea-38c12b45bd45"
77
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
8+
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
89
MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377"
910
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1011
ReTestItems = "817f1d60-ba6b-4fd5-9520-3cf149f6a823"

test/qn_test.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,13 @@ end
141141

142142
@test_throws r"no activators or inhibitors" default_target_function(0, 4)
143143
end
144+
145+
@testitem "Load from BMA" begin
146+
using JSON
147+
bma_models_path = joinpath(@__DIR__, "resources", "bma_models")
148+
149+
for model_path in readdir(bma_models_path; join = true)
150+
qn = QN(model_path)
151+
@test qn isa GraphDynamicalSystem
152+
end
153+
end

0 commit comments

Comments
 (0)