Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 10 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "GraphDynamicalSystems"
uuid = "13529e2e-ed53-56b1-bd6f-420b01fca819"
authors = ["Reuben Gardos Reid <[email protected]>"]
version = "0.0.3"
version = "0.0.4"

[deps]
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
Expand All @@ -18,6 +18,13 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"

[weakdeps]
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"

[extensions]
JSONExt = ["JSON", "MacroTools"]

[compat]
AbstractTrees = "0.4.5"
DocStringExtensions = "0.9.3"
Expand All @@ -27,6 +34,8 @@ HerbConstraints = "0.4"
HerbCore = "0.3.4"
HerbGrammar = "0.6"
HerbSearch = "0.4.1"
JSON = "0.21.4"
MacroTools = "0.5.16"
MLStyle = "0.4.17"
MetaGraphsNext = "0.7"
Random = "1.10"
Expand Down
18 changes: 18 additions & 0 deletions docs/src/91-developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ pkg> activate .
pkg> test
```

Alternatively, if you have
[`ReTestItems.jl`](https://github.com/JuliaTesting/ReTestItems.jl),
[`TestEnv.jl`](https://github.com/JuliaTesting/TestEnv.jl), and
[`Revise.jl`](https://timholy.github.io/Revise.jl/stable) installed, you can use the script
at `test/quick.jl` for rapid testing. This allows you to make changes and immediately run the entire test
suite with those changes. The upside to this setup is that the tests will fail fast, and if
you re-run, the tests that failed on the most recent run will run first. This is great if
you're just iterating on one part of the package and trying to get a handful of tests to
pass. You won't waste time on waiting for other tests to pass and you don't have to manually
select which tests you're actively working on.

```julia-repl
julia> include("test/quick.jl")
```

is all you need to get up and running. Rerunning `include` will rerun the tests as described
above.

## Working on a new issue

We try to keep a linear history in this repo, so it is important to keep your branches up-to-date.
Expand Down
159 changes: 159 additions & 0 deletions ext/JSONExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
module JSONExt
import JSON

using GraphDynamicalSystems: Asynchronous, QualitativeNetwork, default_target_function
using Graphs: SimpleDiGraph, add_edge!, add_vertex!
using MacroTools: @capture, postwalk
using MetaGraphsNext: MetaGraph, inneighbor_labels

function nested_dicts_keys_to_lowercase(d)
if d isa AbstractDict
return Dict([lowercase(k) => nested_dicts_keys_to_lowercase(v) for (k, v) in d])
elseif d isa AbstractVector
return [nested_dicts_keys_to_lowercase(v) for v in d]
else
return d
end
end

function sanitize_formula(f)
# surround variable names with quotes
return replace(f, r"var\(([^\)]+)\)" => s"var(\"\1\")")
end

function entity_name_from_in_neighbors(entity, in_neighbors)
# the formulas can reference their incoming edges
# with either the name of the neighbor entity or
# its id
e_id = tryparse(Int, entity)

entity_name = [
Symbol("$(name)_$id") for
(id, name, _) in in_neighbors if isnothing(e_id) ? name == entity : id == e_id
]

if length(entity_name) != 1
error(
"""
Error while constructing name for entity: $entity, with in neighbors: \
$in_neighbors. There are more than one incoming neighbor entities with the same \
name. To fix this error, remove the erroneous relationships from the JSON file, \
or reference the entity by id (like `var(3)`).
""",
)
end
return only(entity_name)
end

function create_target_function(
variable::Dict,
in_neighbor_ids::Vector{Int},
id_to_name::Dict,
mg::MetaGraph,
)
formula = Meta.parse(sanitize_formula(variable["formula"]))
in_neighbor_names = getindex.((id_to_name,), in_neighbor_ids)
in_neighbor_types = getindex.((mg.edge_data,), in_neighbor_ids, (variable["id"],))
in_neighbors = zip(in_neighbor_ids, in_neighbor_names, in_neighbor_types)

if isnothing(formula) # default target function
if length(in_neighbor_ids) == 0
@warn "$(variable["name"]) has no inputs, defaulting formula to lowest value ($(variable["rangefrom"]))."
return variable["rangefrom"]
else
activators = [
Symbol("$(name)_$id") for
(id, name, ty) in in_neighbors if ty == "Activator"
]
inhibitors = [
Symbol("$(name)_$id") for
(id, name, ty) in in_neighbors if ty == "Inhibitor"
]
return default_target_function(
variable["rangefrom"],
variable["rangeto"],
activators,
inhibitors,
)
end
else # custom target function
return postwalk(
x ->
@capture(x, var(v_String)) ?
:($(entity_name_from_in_neighbors(v, in_neighbors))) : x,
formula,
)
end
end

function to_from_variable_id(r, from_to)
k = "$(from_to)variable"
k_w_id = k * "id"

if haskey(r, k)
return r[k]
elseif haskey(r, k_w_id)
return r[k_w_id]
else
error("""
Neither alternative key was found to retrieve the edge variable id. The \
model file is not using the expected structure for BMA models.
""")
end
end

function QualitativeNetwork(bma_file_path::AbstractString)
json_def = JSON.parse(read(bma_file_path, String))

json_def = nested_dicts_keys_to_lowercase(json_def)
model = json_def["model"]
variables = model["variables"]
relationships = model["relationships"]

id_to_name = Dict([v["id"] => v["name"] for v in variables])
names = [Symbol("$(v["name"])_$(v["id"])") for v in variables]
mg = MetaGraph(SimpleDiGraph(), Int, Union{Expr,Integer,Symbol}, String)

foreach(variables) do v
id = v["id"]
name = v["name"]
# adding an empty expression: :()
# because we need to construct the interaction graph
# first before parsing the functions correctly
added = add_vertex!(mg, id, :())
if !added
error(
"""
Failed to add the entity (\"$name\", id: #$id) from the input file while \
constructing the QN. Check that there is only one entity in the model with \
the id #$id.
""",
)
end
end

foreach(relationships) do r
from = to_from_variable_id(r, "from")
to = to_from_variable_id(r, "to")
type_of_edge = r["type"]
added = add_edge!(mg, from, to, type_of_edge)
if !added
@warn """
Encountered a duplicate relationship between entities (from: \
$(id_to_name[from]), #$from; to: $(id_to_name[to]), #$to) while constructing \
the QN.
"""
end
end

formulas = Union{Expr,Integer,Symbol}[
create_target_function(v, collect(inneighbor_labels(mg, v["id"])), id_to_name, mg) for v in variables
]

# @show formulas
# formulas = Union{Expr,Integer,Symbol}[v["formula"] for v in variables]
domains = [v["rangefrom"]:v["rangeto"] for v in variables]
#
return QualitativeNetwork(names, formulas, domains; schedule = Asynchronous)
end
end
1 change: 1 addition & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
HerbCore = "2b23ba43-8213-43cb-b5ea-38c12b45bd45"
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
ReTestItems = "817f1d60-ba6b-4fd5-9520-3cf149f6a823"
Expand Down
20 changes: 20 additions & 0 deletions test/qn_test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,23 @@ end

@test_throws r"no activators or inhibitors" default_target_function(0, 4)
end

@testitem "Load from BMA" begin
using JSON
bma_models_path = joinpath(@__DIR__, "resources", "bma_models")
good_models = joinpath(bma_models_path, "well_formed_examples")

for model_path in readdir(good_models; join = true)
qn = QN(model_path)
@test qn isa GraphDynamicalSystem
end

bad_models = joinpath(bma_models_path, "error_examples")

@test_throws "Neither alternative" QN(joinpath(bad_models, "bad_edge_key.json"))
@test_throws "Failed to add" QN(joinpath(bad_models, "duplicate_entity_ids.json"))
@test_throws "Error while constructing name for entity" QN(
joinpath(bad_models, "multiple_incoming_edges_same_name.json"),
)

end
18 changes: 18 additions & 0 deletions test/quick.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#
# `include("test/quick.jl")` for quick testing
#
# Assumes you have TestEnv and ReTestItems installed in your
# global Julia environment.
#
using TestEnv
using ReTestItems
using GraphDynamicalSystems

TestEnv.activate() do
runtests(
GraphDynamicalSystems,
name = r"^(?!Code).+$",
failfast = true,
failures_first = true,
)
end
103 changes: 103 additions & 0 deletions test/resources/bma_models/error_examples/bad_edge_key.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{
"Layout": {
"AnnotatedGridCells": [],
"Containers": [
{
"Id": 1,
"Name": "C0",
"PositionX": 2,
"PositionY": 0,
"Size": 1
}
],
"Description": "",
"Variables": [
{
"Angle": 0,
"CellX": null,
"CellY": null,
"ContainerId": 1,
"Description": "",
"Id": 1,
"Name": "a",
"PositionX": 630.7,
"PositionY": 47.34285714285714,
"Type": "Default"
},
{
"Angle": 0,
"CellX": null,
"CellY": null,
"ContainerId": 1,
"Description": "",
"Id": 2,
"Name": "b",
"PositionX": 583.2,
"PositionY": 178.77142857142857,
"Type": "Default"
},
{
"Angle": 0,
"CellX": null,
"CellY": null,
"ContainerId": 1,
"Description": "",
"Id": 3,
"Name": "c",
"PositionX": 678.2,
"PositionY": 162.34285714285716,
"Type": "Default"
}
]
},
"Model": {
"Name": "ToyModelStable",
"Relationships": [
{
"FromVariableError": 1,
"Id": 1,
"ToVariableError": 2,
"Type": "Activator"
},
{
"ErrorFromVariable": 2,
"ErrorToVariable": 3,
"Id": 2,
"Type": "Activator"
},
{
"FromErrorVariable": 3,
"Id": 3,
"ToErrorVariable": 1,
"Type": "Inhibitor"
}
],
"Variables": [
{
"Formula": "",
"Id": 1,
"Name": "a",
"RangeFrom": 0,
"RangeTo": 4
},
{
"Formula": "",
"Id": 2,
"Name": "b",
"RangeFrom": 0,
"RangeTo": 4
},
{
"Formula": "",
"Id": 3,
"Name": "c",
"RangeFrom": 0,
"RangeTo": 4
}
]
},
"ltl": {
"operations": [],
"states": []
}
}
Loading
Loading