diff --git a/Project.toml b/Project.toml index c1c8f17..d39c72a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,13 +1,12 @@ name = "GraphDynamicalSystems" uuid = "13529e2e-ed53-56b1-bd6f-420b01fca819" authors = ["Reuben Gardos Reid <5456207+ReubenJ@users.noreply.github.com>"] -version = "0.0.2" +version = "0.0.3" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4" -FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" HerbConstraints = "1fa96474-3206-4513-b4fa-23913f296dfc" HerbCore = "2b23ba43-8213-43cb-b5ea-38c12b45bd45" @@ -17,15 +16,12 @@ MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" -SoleLogics = "b002da8f-3cb3-4d91-bbe3-2953433912b5" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" -Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] AbstractTrees = "0.4.5" DocStringExtensions = "0.9.3" DynamicalSystemsBase = "3.13.2" -FileIO = "1" Graphs = "1.12" HerbConstraints = "0.4" HerbCore = "0.3.4" @@ -35,7 +31,5 @@ MLStyle = "0.4.17" MetaGraphsNext = "0.7" Random = "1.10" SciMLBase = "2.74.1" -SoleLogics = "0.13" StaticArrays = "1.9.12" -Statistics = "1.10" -julia = "1.6" +julia = "1.10" diff --git a/src/GraphDynamicalSystems.jl b/src/GraphDynamicalSystems.jl index 025dc64..8f0f1de 100644 --- a/src/GraphDynamicalSystems.jl +++ b/src/GraphDynamicalSystems.jl @@ -2,20 +2,19 @@ module GraphDynamicalSystems using DocStringExtensions - -include("boolean_networks.jl") -export BooleanNetworks +include("gds_interface.jl") +export ScheduleStyle, Asynchronous, Synchronous, get_schedule, get_state, get_graph include("qualitative_networks.jl") export QualitativeNetwork, QN, build_qn_grammar, - update_functions_to_network, + update_functions_to_interaction_graph, sample_qualitative_network, - max_level, - components, + entities, + get_domain, target_functions, interpret, - aqn + create_qn_system end diff --git a/src/boolean_networks.jl b/src/boolean_networks.jl deleted file mode 100644 index 81155c9..0000000 --- a/src/boolean_networks.jl +++ /dev/null @@ -1,150 +0,0 @@ -""" - -$(EXPORTS) -""" -module BooleanNetworks - -using DocStringExtensions -using MetaGraphsNext: MetaGraph, add_edge!, SimpleDiGraph, nv, labels -using DynamicalSystemsBase: ArbitrarySteppable, current_parameters, initial_state -using SoleLogics: - Formula, - Atom, - ExplicitAlphabet, - randformula, - normalize, - subformulas, - ∧, - ∨, - ¬, - interpret, - TruthDict, - BooleanTruth, - ⊤ -using Random: seed! -using FileIO: load -import SciMLBase - -""" - $(TYPEDSIGNATURES) - -Return a random Boolean network with `n` nodes. -""" -function sample_boolean_network(n::Int, depth::Int = 2, seed::Int = 0; tactic = normalize) - # Create a SoleLogics Atom for each node - alphabet = ExplicitAlphabet(Atom.(1:n)) - operators = [∧, ∨, ¬] - formulas = [tactic(randformula(depth, alphabet, operators; rng = seed + i)) for i = 1:n] - - bn = update_functions_to_network(formulas) - - return bn -end - -""" - $(TYPEDSIGNATURES) -""" -function update_functions_to_network(update_functions::AbstractVector{<:Formula}) - network = MetaGraph(SimpleDiGraph(); label_type = Int, vertex_data_type = Formula) - - for (i, f) in enumerate(update_functions) - network[i] = f - end - - for (i, f) in enumerate(update_functions) - atoms = filter(x -> isa(x, Atom), subformulas(f)) - for atom in atoms - j = atom.value - add_edge!(network, i, j) - end - end - - return network -end - -""" - $(TYPEDEF) - -$(FIELDS) -""" -mutable struct BooleanNetwork - "The structure and update functions of the network" - graph::MetaGraph - "The state of the network" - state::AbstractVector{Int} -end - -""" - $(TYPEDSIGNATURES) -""" -function truth_dict_from_state(state::AbstractVector{Int}, labels::AbstractVector) - return TruthDict(Dict(l => s for (l, s) in zip(labels, state))) -end - -""" - $(TYPEDSIGNATURES) - -Update step for an asynchronous Boolean network (see [`abn`](@ref)). At each step, -It selects a node in the network and applies its update function, updating -the state for the selected node with the update function's output. -""" -function abn_step!(model::BooleanNetwork) - vertex_labels = collect(labels(model.graph)) - i = rand(vertex_labels) - fᵢ = model.graph[i] - td = truth_dict_from_state(model.state, vertex_labels) - u₍ᵢ₊₁₎ = interpret(fᵢ, td) - state_index = findfirst(isequal(i), vertex_labels) - model.state[state_index] = Int(u₍ᵢ₊₁₎.flag) -end - -extract_state(model::BooleanNetwork) = model.state -extract_parameters(model::BooleanNetwork) = model.graph -reset_model!(model::BooleanNetwork, u, _) = model.state .= u - -function SciMLBase.reinit!( - ds::ArbitrarySteppable{<:AbstractVector{<:Real},<:BooleanNetwork}, - u::AbstractVector{<:Real} = initial_state(ds); - p = current_parameters(ds), - t0 = 0, # t0 is not used but required for downstream. -) - ds.reinit(ds.model, u, p) - ds.t[] = 0 - return ds -end - -""" - $(TYPEDSIGNATURES) - -Create an asynchronous Boolean network (ABN) with the given `network`, -`initial_state`. The update step for an asynchronous Boolean network -is to choose a random node and update its state, one node at a time. -The random choice is uniform over all nodes. See [`abn_step!`](@ref). -""" -function abn(network::MetaGraph, initial_state::AbstractVector{Int}) - model = BooleanNetwork(network, initial_state) - - return ArbitrarySteppable( - model, - abn_step!, - extract_state, - extract_parameters, - reset_model!, - isdeterministic = false, - ) -end - -""" - $(TYPEDSIGNATURES) - -Create an asynchronous Boolean network with a random initial state. -""" -function abn(network::MetaGraph; seed::Int = 42) - n = nv(network) - seed!(seed) - initial_state = rand(0:1, n) - return abn(network, initial_state) -end - - -end diff --git a/src/gds_interface.jl b/src/gds_interface.jl new file mode 100644 index 0000000..8a58656 --- /dev/null +++ b/src/gds_interface.jl @@ -0,0 +1,54 @@ +import DynamicalSystemsBase.get_state +using MetaGraphsNext: labels + +abstract type ScheduleStyle end +struct Asynchronous <: ScheduleStyle end +struct Synchronous <: ScheduleStyle end + +abstract type GraphDynamicalSystem{N,S} end +const GDS = GraphDynamicalSystem + +""" + $(TYPEDSIGNATURES) + +Get the number of entities `N` in the GDS. +""" +function get_n_entities(::GDS{N,S}) where {N,S} + return N +end + +""" + $(TYPEDSIGNATURES) + +Get the schedule for the GDS. +""" +function get_schedule(::GDS{N,S}) where {N,S} + return S +end + +""" + $(TYPEDSIGNATURES) + +Get the underlying graph of the GDS. +""" +function get_graph(gds::GDS) + return gds.graph +end + +""" + $(TYPEDSIGNATURES) + +List all entities in `gds`. +""" +function entities(gds::GDS) + return collect(labels(get_graph(gds))) +end + +""" + $(TYPEDSIGNATURES) + +Get the state of the GDS. +""" +function get_state(gds::GDS) + return gds.state +end diff --git a/src/qualitative_networks.jl b/src/qualitative_networks.jl index f4bb2f8..c266a1d 100644 --- a/src/qualitative_networks.jl +++ b/src/qualitative_networks.jl @@ -1,16 +1,15 @@ import DynamicalSystemsBase: get_state, set_state! +import SciMLBase using AbstractTrees: Leaves using DynamicalSystemsBase: ArbitrarySteppable, current_parameters, initial_state -using HerbConstraints: addconstraint!, DomainRuleNode, VarNode, Ordered, Forbidden +using HerbConstraints: DomainRuleNode, Forbidden, Ordered, VarNode, addconstraint! using HerbCore: AbstractGrammar, RuleNode, get_rule -using HerbGrammar: add_rule!, rulenode2expr, @csgrammar +using HerbGrammar: @csgrammar, add_rule!, rulenode2expr using HerbSearch: rand using MLStyle: @match -using MetaGraphsNext: MetaGraph, SimpleDiGraph, add_edge!, nv, labels -import SciMLBase -using StaticArrays: MVector -using SoleLogics: Atom, value +using MetaGraphsNext: MetaGraph, SimpleDiGraph, add_edge!, labels, nv +using StaticArrays: MVector, SVector const base_qn_grammar = @csgrammar begin Val = Val + Val @@ -92,85 +91,115 @@ function build_qn_grammar(entity_names, constants = default_qn_constants) return g end +struct Entity{I} + target_function::Any + # _f::Any + domain::UnitRange{I} +end + +get_target_function(e::Entity) = e.target_function +get_domain(e::Entity) = e.domain + """ $(TYPEDSIGNATURES) """ -function update_functions_to_network( - update_functions::AbstractDict{Symbol,<:Any}, - grammar::AbstractGrammar, +function update_functions_to_interaction_graph( + entities::AbstractVector{Symbol}, + update_functions::AbstractVector{Union{Integer,Symbol,Expr}}, + domains::AbstractVector{UnitRange{Int}}; + schedule = Synchronous, ) - network = MetaGraph( + graph = MetaGraph( SimpleDiGraph(); label_type = Symbol, - vertex_data_type = Union{Symbol,Expr,Int,Atom}, - graph_data = grammar, + vertex_data_type = Entity{Int}, + graph_data = schedule, ) - for (e, f) in update_functions - network[e] = f + for (entity, fn, domain) in zip(entities, update_functions, domains) + graph[entity] = Entity{Int}(fn, domain) end - for (e1, f) in update_functions - input_variables = collect(Leaves(f)) - for e2 in input_variables - add_edge!(network, e1, e2) + for (e1, f) in zip(entities, update_functions) + input_entities = collect(Leaves(f)) + for e2 in input_entities + add_edge!(graph, e1, e2) end end - return network + return graph end """ $(TYPEDSIGNATURES) """ -function sample_qualitative_network(entities::AbstractVector{Symbol}, max_eq_depth::Int) +function sample_qualitative_network( + entities::AbstractVector{Symbol}, + domains::AbstractVector{UnitRange{Int}}, + max_eq_depth::Int; + schedule = Synchronous, +) g = build_qn_grammar(entities, default_qn_constants) - update_fns = Dict{Symbol,Union{Symbol,Expr,Int}}([ - e => rulenode2expr(rand(RuleNode, g, :Val, max_eq_depth), g) for e in entities - ]) - graph = update_functions_to_network(update_fns, g) + update_fns = Union{Expr,Integer,Symbol}[ + rulenode2expr(rand(RuleNode, g, :Val, max_eq_depth), g) for _ in entities + ] - return graph -end + qn = QualitativeNetwork(entities, update_fns, domains; schedule = schedule) -""" - $(TYPEDSIGNATURES) -""" -function sample_qualitative_network(size::Int, max_eq_depth::Int) - entities = [Symbol("c$e") for e = 1:size] - sample_qualitative_network(entities, max_eq_depth) + return qn end +sample_qualitative_network(N::Int, args...; kwargs...) = + sample_qualitative_network(Symbol.(('A':'Z')[1:N]), args...; kwargs...) + """ $(TYPEDEF) -A qualitative network model as described in -["Qualitative networks: a symbolic approach to analyze biological -signaling networks"](https://doi.org/10.1186/1752-0509-1-4 -). +A qualitative network model as described in ["Qualitative networks: a symbolic approach to +analyze biological signaling networks"](https://doi.org/10.1186/1752-0509-1-4). + +This implementation encompasses both the synchronous and asynchonous cases. In the paper, it +is assumed that the synchronous case is used. As such, the default constructor uses a +synchronous schedule. $(FIELDS) -Systems that include the model semantics wrap around this struct -with an [`ArbitrarySteppable`](https://juliadynamics.github.io/DynamicalSystems.jl/stable/tutorial/#DynamicalSystemsBase.ArbitrarySteppable) -from [`DynamicalSystems`](https://juliadynamics.github.io/DynamicalSystems.jl/stable/). -See [`aqn`](@ref) for an example. +Systems that include the model semantics wrap around this struct with an +[`ArbitrarySteppable`](https://juliadynamics.github.io/DynamicalSystems.jl/stable/tutorial/#DynamicalSystemsBase.ArbitrarySteppable) +from [`DynamicalSystems`](https://juliadynamics.github.io/DynamicalSystems.jl/stable/). See +[`aqn`](@ref) for an example. """ -struct QualitativeNetwork{N,C} +struct QualitativeNetwork{N,S} <: GraphDynamicalSystem{N,S} "Graph containing the topology and target functions of the network" graph::MetaGraph "State of the network" - state::MVector{C,Int} - "The maximum activation level/state value of any component" - N::Int + state::MVector{N,Int} - function QualitativeNetwork(g, s, N) - if any(s .> N) - error("All values in state must be <= N (N=$N)") - end + function QualitativeNetwork(graph, state; schedule = Synchronous) + N = nv(graph) + return new{N,schedule()}(graph, state) + end +end - return new{N,length(s)}(g, s, N) +function QualitativeNetwork( + entities::AbstractVector{Symbol}, + functions::AbstractVector{Union{Integer,Symbol,Expr}}, + domains; + state = nothing, + schedule = Synchronous, +) + graph = update_functions_to_interaction_graph( + entities, + functions, + domains; + schedule = schedule, + ) + + if isnothing(state) + state = rand.(domains) end + + return QualitativeNetwork(graph, state; schedule) end """ @@ -182,58 +211,65 @@ const QN = QualitativeNetwork """ $(TYPEDSIGNATURES) + +Get the domain of the entity `entity_label` in `qn`. """ -function max_level(qn::QN) - return qn.N -end +function get_domain(qn::QN, entity_label::Symbol) + graph = get_graph(qn) + entity = graph[entity_label] -function _get_component_index(qn::QN, component) - return findfirst(isequal(component), components(qn)) + return get_domain(entity) end """ $(TYPEDSIGNATURES) + +Get all of the domains of the entities in `qn`. """ -function components(qn::QN) - return collect(labels(qn.graph)) +function get_domain(qn::QN) + return get_domain.((qn,), labels(get_graph(qn))) end -""" - $(TYPEDSIGNATURES) -""" -function target_functions(qn::QN) - return Dict([c => fn for (c, (_, fn)) in qn.graph.vertex_properties]) +function _get_entity_index(qn::QN, entity) + return findfirst(isequal(entity), entities(qn)) end + """ $(TYPEDSIGNATURES) """ -get_state(qn::QN) = qn.state +function target_functions(qn::QN) + return Dict([ + c => get_target_function(entity) for + (c, (_, entity)) in get_graph(qn).vertex_properties + ]) +end """ $(TYPEDSIGNATURES) """ function get_state(qn::QN, component) - i = _get_component_index(qn, component) + i = _get_entity_index(qn, component) return qn.state[i] end function _set_state!(qn::QN, component::Symbol, value::Integer) - i = _get_component_index(qn::QN, component::Symbol) + i = _get_entity_index(qn::QN, component::Symbol) qn.state[i] = value end """ $(TYPEDSIGNATURES) """ -function set_state!(qn::QN, component::Symbol, value::Integer) - if value > max_level(qn) +function set_state!(qn::QN, entity::Symbol, value::Integer) + max_for_entity = maximum(get_domain(qn, entity)) + if value > max_for_entity error( - "Value ($value) cannot be larger than the QN's maximum level (N=$(max_level(qn)))", + "Value ($value) cannot be larger than the maximum level for $entity ($(max_for_entity))", ) end - _set_state!(qn, component, value) + _set_state!(qn, entity, value) end """ @@ -256,7 +292,6 @@ function interpret(e::Union{Expr,Symbol,Int}, qn::QN) _ => error("Unhandled Expr in `interpret`: $e") end end -interpret(e::Atom, qn::QN) = get_state(qn, Symbol(value(e))) """ $(TYPEDSIGNATURES) @@ -265,11 +300,16 @@ Returns the limited value of `next_value` which is at most 1 different than `pre It is also never negative, or larger than `N`. """ -function limit_change(prev_value::Integer, next_value::Integer, N::Integer) +function limit_change( + prev_value::Integer, + next_value::Integer, + min_level::Integer, + max_level::Integer, +) if next_value > prev_value - limited_value = min(prev_value + 1, N) + limited_value = min(prev_value + 1, max_level) elseif next_value < prev_value - limited_value = max(prev_value - 1, 0) + limited_value = max(prev_value - 1, min_level) else limited_value = next_value end @@ -281,15 +321,23 @@ end $(TYPEDSIGNATURES) """ function async_qn_step!(qn::QN) - vertex_labels = collect(labels(qn.graph)) - c_i = rand(vertex_labels) - t = target_functions(qn)[c_i] - old_state = get_state(qn, c_i) + entity_labels = collect(labels(qn.graph)) + entity = rand(entity_labels) + (min_level, max_level) = extrema(get_domain(qn, entity)) + t = target_functions(qn)[entity] + old_state = get_state(qn, entity) new_state = interpret(t, qn) - new_state = isnan(new_state) ? 0 : new_state - new_state = isinf(new_state) ? max_level(qn) : new_state - limited_state = limit_change(old_state, floor(Int, new_state), max_level(qn)) - set_state!(qn, c_i, limited_state) + new_state = isnan(new_state) ? min_level : new_state + new_state = isinf(new_state) ? max_level : new_state + limited_state = limit_change(old_state, floor(Int, new_state), min_level, max_level) + set_state!(qn, entity, limited_state) +end + +""" + $(TYPEDSIGNATURES) +""" +function sync_qn_step!(qn::QN) + throw(ErrorException("Synchronous step function not yet implemented")) end extract_state(model::QN) = model.state @@ -313,24 +361,15 @@ end Construct an asynchronous [`QualitativeNetwork`](@ref) system using the [`async_qn_step!`](@ref) as a step function. """ -function aqn(network::MetaGraph, initial_state::AbstractVector{Int}, max_level::Int) - model = QualitativeNetwork(network, initial_state, max_level) +function create_qn_system(qn::QN) + step_fn = get_schedule(qn) == Asynchronous() ? async_qn_step! : sync_qn_step! return ArbitrarySteppable( - model, - async_qn_step!, + qn, + step_fn, extract_state, extract_parameters, reset_model!, isdeterministic = false, ) end - -""" - $(TYPEDSIGNATURES) -""" -function aqn(network::MetaGraph, max_level::Int) - n_components = nv(network) - initial_state = rand(0:max_level, n_components) - return aqn(network, initial_state, max_level) -end diff --git a/test/Project.toml b/test/Project.toml index 609f792..fe534bd 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -7,5 +7,6 @@ HerbCore = "2b23ba43-8213-43cb-b5ea-38c12b45bd45" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +ReTestItems = "817f1d60-ba6b-4fd5-9520-3cf149f6a823" SoleLogics = "b002da8f-3cb3-4d91-bbe3-2953433912b5" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/qn_test.jl b/test/qn_test.jl new file mode 100644 index 0000000..321f57f --- /dev/null +++ b/test/qn_test.jl @@ -0,0 +1,119 @@ +@testsetup module RandomSetup +using Random: seed! +seed!(42) +end + +@testsetup module ExampleQN +export qn_size, max_eq_depth, domains, qn +using GraphDynamicalSystems + +qn_size = 3 +max_eq_depth = 3 +domains = [1:5, 1:5, 1:5] +qn = sample_qualitative_network(qn_size, domains, max_eq_depth) +end + +@testitem "QN Grammar Creation" begin + entities = [:a, :b, :c] + constants = [i for i = 1:10] + g = build_qn_grammar(entities, constants) + + @test issubset(Set(entities), Set(g.rules)) + @test issubset(Set(constants), Set(g.rules)) + + g2 = build_qn_grammar(Symbol[], Integer[]) + + @test isempty(intersect(Set(g2.rules), Set(entities))) + @test isempty(intersect(Set(g2.rules), Set(constants))) +end + +@testitem "QN Sampling" setup = [RandomSetup, ExampleQN] begin + using Graphs: ne, nv + graph = get_graph(qn) + + @test nv(graph) == qn_size + @test ne(graph) > 0 +end + +@testitem "QN properties, fields" setup = [RandomSetup, ExampleQN] begin + using DynamicalSystemsBase: step!, get_state, set_state! + + set_state!(qn, :A, 1) + + @test length(entities(qn)) == qn_size + + @test length(target_functions(qn)) == qn_size + + @test all(get_state(qn) .<= maximum.(get_domain(qn))) + + @test get_state(qn, :A) == 1 + + @test_throws r"max" set_state!(qn, :A, 6) +end + +@testitem "QN Construction" setup = [RandomSetup, ExampleQN] begin + initial_state_beyond_domain = [5, 5, 5] + + # @test_throws r"<=" set_state!(qn, en +end + +@testitem "Target Function" setup = [RandomSetup, ExampleQN] begin + using DynamicalSystemsBase: step!, get_state, set_state! + set_state!(qn, :A, 1) + set_state!(qn, :B, 1) + set_state!(qn, :C, 1) + + # All state values should be 1, so adding two of them == 2, etc. + # The size of the test network is 3, so there should be A, B, C + # as available entities to work with. + @test interpret(:(A + B), qn) == 2 + @test interpret(:(A - B), qn) == 0 + set_state!(qn, :B, 2) + @test interpret(:(A / B), qn) == 0.5 + @test interpret(:(A / 2), qn) == 0.5 + @test interpret(:(Min(A, B)), qn) == 1 + @test interpret(:(Max(A, B)), qn) == 2 + @test interpret(:(Ceil(A / B)), qn) == 1 + @test interpret(:(Floor(A / B)), qn) == 0 + @test_throws r"Unhandled" interpret(:(nonexistent_function(A)), qn) +end + +@testitem "Async QN" setup = [RandomSetup] begin + using DynamicalSystemsBase: step!, get_state, set_state! + qn_size = 3 + max_eq_depth = 3 + + for N = 2:5 # a few different levels of N + for _ = 1:100 # 100 different initializations + domains = [1:N for _ = 1:qn_size] + async_qn = sample_qualitative_network( + qn_size, + domains, + max_eq_depth; + schedule = Asynchronous, + ) + async_qn_system = create_qn_system(async_qn) + step!(async_qn_system, 100) + @test all(get_state(async_qn_system.model) .<= maximum.(domains)) + end + end + +end + +@testitem "Get attractors" setup = [RandomSetup, ExampleQN] begin + using Attractors: AttractorsViaRecurrences, basins_of_attraction + qn_size = 3 + max_eq_depth = 3 + N = 3 + domains = [1:N for _ = 1:qn_size] + + async_qn = + sample_qualitative_network(qn_size, domains, max_eq_depth; schedule = Asynchronous) + async_qn_system = create_qn_system(async_qn) + + grid = Tuple(range(0, 1) for _ = 1:qn_size) + + mapper = AttractorsViaRecurrences(async_qn_system, grid) + + basins = basins_of_attraction(mapper, grid) +end diff --git a/test/quality_tests.jl b/test/quality_tests.jl new file mode 100644 index 0000000..aa14df8 --- /dev/null +++ b/test/quality_tests.jl @@ -0,0 +1,9 @@ +@testitem "Code quality (Aqua.jl)" begin + using Aqua + Aqua.test_all(GraphDynamicalSystems) +end + +@testitem "Code linting (JET.jl)" begin + using JET + JET.test_package(GraphDynamicalSystems; target_defined_modules = true) +end diff --git a/test/runtests.jl b/test/runtests.jl index 7d0839d..a9f9a8c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,33 +1,3 @@ -using Aqua -using GraphDynamicalSystems -using JET -using Test +using ReTestItems, GraphDynamicalSystems - -#= -Don't add your tests to runtests.jl. Instead, create files named - - test-title-for-my-test.jl - -The file will be automatically included inside a `@testset` with title "Title For My Test". -=# -@testset "GraphDynamicalSystems.jl" begin - @testset "Code quality (Aqua.jl)" begin - Aqua.test_all(GraphDynamicalSystems) - end - @testset "Code linting (JET.jl)" begin - JET.test_package(GraphDynamicalSystems; target_defined_modules = true) - end - - for (root, dirs, files) in walkdir(@__DIR__) - for file in files - if isnothing(match(r"^test-.*\.jl$", file)) - continue - end - title = titlecase(replace(splitext(file[6:end])[1], "-" => " ")) - @testset "$title" begin - include(file) - end - end - end -end +runtests(GraphDynamicalSystems) diff --git a/test/test-bn.jl b/test/test-bn.jl deleted file mode 100644 index e81b43d..0000000 --- a/test/test-bn.jl +++ /dev/null @@ -1,34 +0,0 @@ -using MetaGraphsNext: nv, MetaGraph -using DynamicalSystemsBase: trajectory -using SoleLogics: cnf, AbstractSyntaxStructure, LeftmostLinearForm, ∧ - -@testset "Boolean Network Sampling" begin - bn = BooleanNetworks.sample_boolean_network(10) - @test nv(bn) == 10 - @test isa(bn, MetaGraph) - - bn_cnf = BooleanNetworks.sample_boolean_network(20; tactic = cnf) - @test nv(bn_cnf) == 20 - @test typeof(bn_cnf[1]) <: LeftmostLinearForm{typeof(∧),<:AbstractSyntaxStructure} -end - -@testset "Asynchronous Boolean Network Creation" begin - - @testset "with explicit initial state" begin - bn = BooleanNetworks.sample_boolean_network(10) - initial_state = rand(0:1, 10) - abn = BooleanNetworks.abn(bn, deepcopy(initial_state)) - traj = trajectory(abn, 10) - @test traj[1][1] == initial_state - @test length(traj[1]) == 11 - end - - @testset "with random initial state" begin - bn = BooleanNetworks.sample_boolean_network(10) - abn₁ = BooleanNetworks.abn(bn; seed = 10) - traj₁ = trajectory(abn₁, 10) - abn₂ = BooleanNetworks.abn(bn; seed = 10) - traj₂ = trajectory(abn₂, 10) - @test traj₁[1] == traj₂[1] - end -end diff --git a/test/test-qn.jl b/test/test-qn.jl deleted file mode 100644 index b7e7fcd..0000000 --- a/test/test-qn.jl +++ /dev/null @@ -1,92 +0,0 @@ -using Attractors: AttractorsViaRecurrences, basins_of_attraction -using DynamicalSystemsBase: step!, get_state, set_state! -using Graphs: ne, nv -using Random: seed! - -seed!(42) - -@testset "QN Grammar Creation" begin - entities = [:a, :b, :c] - constants = [i for i = 1:10] - g = build_qn_grammar(entities, constants) - - @test issubset(Set(entities), Set(g.rules)) - @test issubset(Set(constants), Set(g.rules)) - - g2 = build_qn_grammar(Symbol[], Integer[]) - - @test isempty(intersect(Set(g2.rules), Set(entities))) - @test isempty(intersect(Set(g2.rules), Set(constants))) -end - -size = 3 -max_eq_depth = 3 -N = 5 -network = sample_qualitative_network(size, max_eq_depth) -initial_state = ones(size) -qn = QualitativeNetwork(network, initial_state, N) - -@testset "QN Sampling" begin - @test nv(network) == size - @test ne(network) > 0 -end - -@testset "QN properties, fields" begin - @test length(components(qn)) == size - - @test length(target_functions(qn)) == size - - @test all(get_state(qn) .<= max_level(qn)) - - @test get_state(qn, components(qn)[1]) == 1 - - @test_throws r"max" set_state!(qn, Symbol(1), 6) -end - -@testset "QN Construction" begin - basic_grammar = build_qn_grammar(Symbol[], Integer[]) - lower_N = 3 - initial_state_higher_than_N = [5, 5, 5] - - @test_throws r"<=" QN(basic_grammar, initial_state_higher_than_N, lower_N) -end - -@testset "Target Function" begin - # All state values should be 1, so adding two of them == 2, etc. - # The size of the test network is 3, so there should be c1, c2, c3 - # as available components to work with. - @test interpret(:(c1 + c2), qn) == 2 - @test interpret(:(c1 - c2), qn) == 0 - set_state!(qn, :c2, 2) - @test interpret(:(c1 / c2), qn) == 0.5 - @test interpret(:(c1 / 2), qn) == 0.5 - @test interpret(:(Min(c1, c2)), qn) == 1 - @test interpret(:(Max(c1, c2)), qn) == 2 - @test interpret(:(Ceil(c1 / c2)), qn) == 1 - @test interpret(:(Floor(c1 / c2)), qn) == 0 - @test_throws r"Unhandled" interpret(:(nonexistent_function(c1)), qn) -end - -@testset "Async QN" begin - for N = 2:5 # a few different levels of N - for _ = 1:100 # 100 different initializations - async_qn = aqn(network, N) - step!(async_qn, 100) - @test all(get_state(async_qn.model) .<= N) - end - end - -end - -@testset "Get attractors" begin - n_entities = 3 - qn = sample_qualitative_network(n_entities, 2) - - async_qn = aqn(qn, 1) - - grid = Tuple(range(0, 1) for _ = 1:n_entities) - - mapper = AttractorsViaRecurrences(async_qn, grid) - - basins = basins_of_attraction(mapper, grid) -end