diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 5b69b65..65fae2d 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -22,7 +22,6 @@ jobs: fail-fast: false matrix: version: - - "lts" - "1" os: - ubuntu-latest diff --git a/Project.toml b/Project.toml index cd38a17..fb88491 100644 --- a/Project.toml +++ b/Project.toml @@ -1,32 +1,27 @@ name = "GraphDynamicalSystems" uuid = "13529e2e-ed53-56b1-bd6f-420b01fca819" -version = "0.0.9" +version = "0.0.10" authors = ["Reuben Gardos Reid <5456207+ReubenJ@users.noreply.github.com>"] [workspace] -projects = ["test"] +projects = ["docs", "test", "implementations/QualitativeNetworks", "implementations/BooleanNetworks"] [deps] -AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" -DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" + +[weakdeps] +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" -MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" -MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" -SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" -TermInterface = "8ea1fca8-c5ef-4a55-8b96-4e9afe9c9a3c" + +[extensions] +AbstractTreesExt = "AbstractTrees" +DynamicalSystemsBaseExt = "DynamicalSystemsBase" +GraphsExt = "Graphs" [compat] AbstractTrees = "0.4.5" -Compat = "4.18.1" -DocStringExtensions = "0.9.3" -DynamicalSystemsBase = "3.10" -Graphs = "1.12" -MLStyle = "0.4.17" -MacroTools = "0.5.16" -MetaGraphsNext = "0.7, 0.8" -SciMLBase = "2.74.1, 3" -TermInterface = "2.0.0" -julia = "1.10" +Compat = "3.47,4.10" +DynamicalSystemsBase = "3.19.3" +Graphs = "1.14.0" +julia = "1.12" diff --git a/docs/Project.toml b/docs/Project.toml index 7a8d108..6ba4e3d 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,7 +2,6 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" GraphDynamicalSystems = "13529e2e-ed53-56b1-bd6f-420b01fca819" LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" -Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" [compat] Documenter = "1" diff --git a/docs/make.jl b/docs/make.jl index 71aec01..b08de32 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,7 +1,3 @@ -using Revise - -Revise.revise() - using GraphDynamicalSystems using Documenter diff --git a/docs/src/95-reference.md b/docs/src/95-reference.md index e6903cb..c78f648 100644 --- a/docs/src/95-reference.md +++ b/docs/src/95-reference.md @@ -13,5 +13,11 @@ Pages = ["95-reference.md"] ``` ```@autodocs -Modules = [GraphDynamicalSystems] +Modules = [ + GraphDynamicalSystems, + GraphDynamicalSystems.Interface, + GraphDynamicalSystems.Schedule, + GraphDynamicalSystems.Conversions, + GraphDynamicalSystems.Constructors +] ``` diff --git a/ext/AbstractTreesExt.jl b/ext/AbstractTreesExt.jl new file mode 100644 index 0000000..4168624 --- /dev/null +++ b/ext/AbstractTreesExt.jl @@ -0,0 +1,9 @@ +module AbstractTreesExt +import AbstractTrees as AT +import GraphDynamicalSystems as GDS + +function GDS.Constructors.vertex_function_to_edgelist(vertex, fn, all_vertices) + recurse_if = x -> !(x in all_vertices) + return filter(in(all_vertices), collect(AT.PreOrderDFS(recurse_if, fn))) .=> (vertex,) +end +end diff --git a/ext/DynamicalSystemsBaseExt.jl b/ext/DynamicalSystemsBaseExt.jl new file mode 100644 index 0000000..a91085b --- /dev/null +++ b/ext/DynamicalSystemsBaseExt.jl @@ -0,0 +1,50 @@ +module DynamicalSystemsBaseExt +import DynamicalSystemsBase as DSB +import GraphDynamicalSystems as GDS + +function step!(gds) + v_to_update = GDS.to_update(gds) + current_state = Dict(GDS.vertices(gds) .=> GDS.get_state(gds)) + new_state = v_to_update .=> [f(current_state) for f in GDS.update_function(gds, v_to_update)] + GDS.set_state!(gds, new_state) + return nothing +end + +function extract_state(gds) + return GDS.get_state(gds) +end + +function extract_parameters(::Any) + return () # no parameters exposed by default +end + +function reset_model!(gds, u, ::Any) + GDS.set_state!(gds, round.(Int, u)) + return nothing +end + +function GDS.Conversions.to_arbitrary_steppable(gds) + isdeterministic = GDS.is_deterministic(gds) + + return DSB.ArbitrarySteppable( + gds, + step!, + extract_state, + extract_parameters, + reset_model!; + isdeterministic + ) +end + +function DSB.reinit!(as::DSB.ArbitrarySteppable, u) + as.reinit(as.model, u, ()) + return nothing +end + +# Implements part of the GDS interface for DSB's `DynamicalSystem`s +# Useful to apply methods written over the GDS interface directly to +# some DSB-based object. +GDS.update_function(ds::DSB.DynamicalSystem) = DSB.dynamic_rule(ds) +GDS.get_state(ds::DSB.DynamicalSystem) = DSB.current_state(ds) +GDS.set_state!(ds::DSB.DynamicalSystem, u::AbstractVector{<:Real}) = DSB.set_state!(ds, u) +end diff --git a/ext/GraphsExt.jl b/ext/GraphsExt.jl new file mode 100644 index 0000000..33a9f8c --- /dev/null +++ b/ext/GraphsExt.jl @@ -0,0 +1,13 @@ +module GraphsExt +import Graphs +import GraphDynamicalSystems as GDS + +GDS.vertices(graph::Graphs.AbstractGraph) = Graphs.vertices(graph) +GDS.edges(graph::Graphs.AbstractGraph) = Graphs.edges(graph) + +function GDS.Conversions.to_simple_graph(gds) + v = GDS.vertices(gds) + e = [Graphs.Edge(findfirst(==(s), v), findfirst(==(d), v)) for (s, d) in GDS.edges(gds)] + return Graphs.SimpleDiGraph(e) +end +end diff --git a/implementations/BooleanNetworks/Project.toml b/implementations/BooleanNetworks/Project.toml new file mode 100644 index 0000000..d20bd4c --- /dev/null +++ b/implementations/BooleanNetworks/Project.toml @@ -0,0 +1,20 @@ +name = "BooleanNetworks" +uuid = "97f9906b-beee-4f75-a06a-7a05f392a141" +version = "0.0.1" +authors = ["Reuben Gardos Reid <5456207+ReubenJ@users.noreply.github.com>"] + +[workspace] +projects = ["test"] + +[deps] +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" +GraphDynamicalSystems = "13529e2e-ed53-56b1-bd6f-420b01fca819" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" + +[compat] +AbstractTrees = "0.4.5" +Compat = "4.18.1" +Graphs = "1.14.0" +MetaGraphsNext = "0.8.0" diff --git a/implementations/BooleanNetworks/src/BooleanNetworks.jl b/implementations/BooleanNetworks/src/BooleanNetworks.jl new file mode 100644 index 0000000..6f2c746 --- /dev/null +++ b/implementations/BooleanNetworks/src/BooleanNetworks.jl @@ -0,0 +1,90 @@ +module BooleanNetworks +import AbstractTrees as AT +import GraphDynamicalSystems as GDS +import MetaGraphsNext as MG +using Graphs: Graphs +using Compat: @compat + +@compat public BooleanNetwork + +mutable struct VertexData + update_function::Any + state::Bool +end +GDS.update_function(e::VertexData) = e.update_function +GDS.get_state(e::VertexData) = e.state +function GDS.set_state!(e::VertexData, v::Integer) + return e.state = v +end +GDS.domain(e::VertexData) = false:true + +const BNGraphType{G <: Graphs.AbstractGraph{Int}, L, V <: VertexData} = MG.MetaGraph{Int, G, L, V} + +""" + BooleanNetwork + +A graph dynamical system with Boolean domains. + +Update functions are Boolean functions. The schedule style is `Asynchronous` by +default, but can be specified by passing a different type to the first type +parameter explicitly. + +Alias: [`BN`](@ref). +""" +struct BooleanNetwork{S <: GDS.ScheduleStyle, G <: BNGraphType} + graph::G +end + +""" + BN + +Alias for a [`BooleanNetwork`](@ref). +""" +const BN = BooleanNetwork + +function BooleanNetwork{S}(vs, fns) where {S} + edge_list = GDS.Constructors.vertex_function_to_edgelist(vs .=> fns) + e = [Graphs.Edge(findfirst(==(s), vs), findfirst(==(d), vs)) for (s, d) in edge_list] + simple_graph = Graphs.SimpleDiGraph(e) + vertex_data = vs .=> VertexData.(fns, falses(length(vs))) + edge_data = Tuple.(edge_list) .=> nothing # could add edge sign here + meta_graph = MG.MetaGraph(simple_graph, vertex_data, edge_data) + + return BooleanNetwork{S, typeof(meta_graph)}(meta_graph) +end + +function GDS.schedule_style(::BooleanNetwork{S}) where {S} + return S() +end + +function GDS.domain(bn::BN, vertices = MG.labels(bn.graph)) + return GDS.domain.(getindex.((bn.graph,), vertices)) +end + +function GDS.update_function(bn::BN, vertices = MG.labels(bn.graph)) + return GDS.update_function.(getindex.((bn.graph,), vertices)) +end + +function GDS.get_state(bn::BN, vertices = MG.labels(bn.graph)) + return GDS.get_state.(getindex.((bn.graph,), vertices)) +end + +function GDS.set_state!(bn::BN, state::AbstractVector{<:Integer}) + g = bn.graph + return foreach(((i, v),) -> GDS.set_state!(g[v], state[i]), enumerate(MG.labels(g))) +end + +function GDS.set_state!(bn::BN, vertex_state_pairs::AbstractVector{<:Pair}) + g = bn.graph + return foreach(((v, s),) -> GDS.set_state!(g[v], s), vertex_state_pairs) +end + +function GDS.vertices(bn::BN) + return MG.labels(bn.graph) +end + +function GDS.edges(bn::BN) + return MG.edge_labels(bn.graph) +end + +end diff --git a/implementations/BooleanNetworks/test/Project.toml b/implementations/BooleanNetworks/test/Project.toml new file mode 100644 index 0000000..cdf0e71 --- /dev/null +++ b/implementations/BooleanNetworks/test/Project.toml @@ -0,0 +1,10 @@ +[deps] +Attractors = "f3fd9213-ca85-4dba-9dfd-7fc91308fec7" +BooleanNetworks = "97f9906b-beee-4f75-a06a-7a05f392a141" +DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4" +ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" +GraphDynamicalSystems = "13529e2e-ed53-56b1-bd6f-420b01fca819" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" +ReTestItems = "817f1d60-ba6b-4fd5-9520-3cf149f6a823" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/implementations/BooleanNetworks/test/bn_tests.jl b/implementations/BooleanNetworks/test/bn_tests.jl new file mode 100644 index 0000000..515faba --- /dev/null +++ b/implementations/BooleanNetworks/test/bn_tests.jl @@ -0,0 +1,66 @@ +@testitem "Construct" begin + import BooleanNetworks as BNs + import GraphDynamicalSystems as GDS + + bn = BNs.BN{GDS.Asynchronous}([:x, :y, :z], Union{Symbol, Int, Expr}[:x, :x, :(y & ~x)]) + + @test length(GDS.vertices(bn)) == 3 + @test length(GDS.edges(bn)) == 4 + @test GDS.update_function(bn) == [:x, :x, :(y & ~x)] + @test GDS.update_function(bn, [:z]) == [:(y & ~x)] +end + +@testitem "Convert to SimpleDiGraph" begin + import BooleanNetworks as BNs + import GraphDynamicalSystems as GDS + import Graphs + + bn = BNs.BN{GDS.Asynchronous}([:x, :y, :z], Union{Symbol, Int, Expr}[:x, :x, :(y & ~x)]) + graph = GDS.Conversions.to_simple_graph(bn) + + @test Graphs.nv(graph) == 3 + @test Graphs.ne(graph) == 4 +end + +@testitem "Convert to DynamicalSystem" begin + import BooleanNetworks as BNs + import GraphDynamicalSystems as GDS + import DynamicalSystemsBase as DSB + import MetaGraphsNext as MG + import Graphs + using Attractors: AttractorsViaRecurrences, basins_of_attraction + + vs = [:x, :y, :z] + fns = [ + _ -> true, + state -> state[:x], + state -> state[:y] & ~state[:x], + ] + vd = BNs.VertexData.(fns, zeros(length(vs))) + es = Graphs.Edge.([(1, 1), (1, 2), (1, 3), (2, 3)]) + es_sym = [(:x, :x), (:x, :y), (:x, :z), (:y, :z)] + graph = Graphs.SimpleDiGraph(es) + meta_graph = MG.MetaGraph(graph, vs .=> vd, Tuple.(es_sym) .=> nothing) + bn = BNs.BooleanNetwork{GDS.Asynchronous, typeof(meta_graph)}(meta_graph) + as = GDS.Conversions.to_arbitrary_steppable(bn) + + @test DSB.current_state(as) == [0, 0, 0] + + grid = Tuple(range(0, 1) for _ in 1:3) + + mapper = AttractorsViaRecurrences(as, grid) + + basins = basins_of_attraction(mapper, grid) + @test length(basins.attractors) == 1 + @test collect(keys(basins.attractors)) == [1] + @test first(basins.attractors[1]) == [1, 1, 0] +end + +@testitem "Convert to discrete system" begin + @test_skip false +end + +@testitem "Explicit imports" begin + using ExplicitImports: test_explicit_imports + test_explicit_imports(BooleanNetworks) +end diff --git a/implementations/BooleanNetworks/test/runtests.jl b/implementations/BooleanNetworks/test/runtests.jl new file mode 100644 index 0000000..f784664 --- /dev/null +++ b/implementations/BooleanNetworks/test/runtests.jl @@ -0,0 +1,4 @@ +using ReTestItems: runtests, @testitem +using BooleanNetworks + +runtests(BooleanNetworks) diff --git a/implementations/QualitativeNetworks/Project.toml b/implementations/QualitativeNetworks/Project.toml new file mode 100644 index 0000000..05bc819 --- /dev/null +++ b/implementations/QualitativeNetworks/Project.toml @@ -0,0 +1,18 @@ +name = "QualitativeNetworks" +uuid = "ba61d19f-daf7-42ed-91ab-e8e04fc92a6d" +version = "0.0.1" +authors = ["Reuben Gardos Reid <5456207+ReubenJ@users.noreply.github.com>"] + +[workspace] +projects = ["test"] + +[deps] +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +GraphDynamicalSystems = "13529e2e-ed53-56b1-bd6f-420b01fca819" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" + +[compat] +AbstractTrees = "0.4.5" +Graphs = "1.14.0" +MetaGraphsNext = "0.8.0" diff --git a/implementations/QualitativeNetworks/src/QualitativeNetworks.jl b/implementations/QualitativeNetworks/src/QualitativeNetworks.jl new file mode 100644 index 0000000..6b036dd --- /dev/null +++ b/implementations/QualitativeNetworks/src/QualitativeNetworks.jl @@ -0,0 +1,107 @@ +module QualitativeNetworks +import AbstractTrees as AT # to allow for constructing from symbolic expressions +import GraphDynamicalSystems as GDS +import MetaGraphsNext as MG +using Graphs: Graphs + +""" + VertexData{F, S, D} + +Per-vertex data of a [`QN`](@ref). +""" +mutable struct VertexData{S, D} + update_function::Any + state::S + domain::D +end +GDS.update_function(e::VertexData) = e.update_function +GDS.get_state(e::VertexData) = e.state +function GDS.set_state!(e::VertexData, v::Integer) + return e.state = v +end +GDS.domain(e::VertexData) = e.domain + +const QNGraphType{G <: Graphs.AbstractGraph{Int}, L, V <: VertexData} = MG.MetaGraph{Int, G, L, V} + +""" + QualitativeNetwork + +A graph dynamical system with a domains of finite integer ranges. + +Update functions are functions over the reals but all are truncated to the +domain and rounded to an integer value. +State values of each entity are limited to change by at most 1 per time step. +In some literature, QNs are referred to as "unitary" networks due to this +restriction. + +Alias: [`QN`](@ref). +""" +struct QualitativeNetwork{G <: QNGraphType} + graph::G +end + +""" + QN + +Alias for a [`QualitativeNetwork`](@ref). +""" +const QN = QualitativeNetwork + +""" + QualitativeNetwork(vs, fns, ds) + +Construct a [`QualitativeNetwork`](@ref) with the vertices `vs`, a _symbolic_ +set of functions `fns`, and domains `ds`. + +This constructor constructs the graph structure based on the functions `fns`. +It does so by adding an edge from `src` to `dst` whenever the function for `dst` +mentioned `src`. + +The `fns` must implement the [`AbstractTrees`](https://juliacollections.github.io/AbstractTrees.jl/stable/) interface. +""" +function QualitativeNetwork(vs, fns, ds) + edge_list = GDS.Constructors.vertex_function_to_edgelist(vs .=> fns) + e = [Graphs.Edge(findfirst(==(s), vs), findfirst(==(d), vs)) for (s, d) in edge_list] + simple_graph = Graphs.SimpleDiGraph(e) + vertex_data = vs .=> VertexData.(fns, zeros(length(vs)), ds) + edge_data = Tuple.(edge_list) .=> nothing # could add edge sign here + meta_graph = MG.MetaGraph(simple_graph, vertex_data, edge_data) + + return QualitativeNetwork(meta_graph) +end + +function GDS.schedule_style(qn::QN) + return GDS.Synchronous() +end + +function GDS.domain(qn::QN, vertices = MG.labels(qn.graph)) + return GDS.domain.(getindex.((qn.graph,), vertices)) +end + +function GDS.update_function(qn::QN, vertices = MG.labels(qn.graph)) + return GDS.update_function.(getindex.((qn.graph,), vertices)) +end + +function GDS.get_state(qn::QN, vertices = MG.labels(qn.graph)) + return GDS.get_state.(getindex.((qn.graph,), vertices)) +end + +function GDS.set_state!(qn::QN, state::AbstractVector{<:Integer}) + g = qn.graph + return foreach(((i, v),) -> GDS.set_state!(g[v], state[i]), enumerate(MG.labels(g))) +end + +function GDS.set_state!(qn::QN, vertex_state_pairs::AbstractVector{<:Pair}) + g = qn.graph + return foreach(((v, s),) -> GDS.set_state!(g[v], s), vertex_state_pairs) +end + +function GDS.vertices(qn::QN) + return MG.labels(qn.graph) +end + +function GDS.edges(qn::QN) + return MG.edge_labels(qn.graph) +end + +end diff --git a/implementations/QualitativeNetworks/test/Project.toml b/implementations/QualitativeNetworks/test/Project.toml new file mode 100644 index 0000000..4754cca --- /dev/null +++ b/implementations/QualitativeNetworks/test/Project.toml @@ -0,0 +1,10 @@ +[deps] +Attractors = "f3fd9213-ca85-4dba-9dfd-7fc91308fec7" +DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4" +ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" +GraphDynamicalSystems = "13529e2e-ed53-56b1-bd6f-420b01fca819" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" +QualitativeNetworks = "ba61d19f-daf7-42ed-91ab-e8e04fc92a6d" +ReTestItems = "817f1d60-ba6b-4fd5-9520-3cf149f6a823" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/implementations/QualitativeNetworks/test/qn_tests.jl b/implementations/QualitativeNetworks/test/qn_tests.jl new file mode 100644 index 0000000..234b434 --- /dev/null +++ b/implementations/QualitativeNetworks/test/qn_tests.jl @@ -0,0 +1,77 @@ +@testitem "Construct" begin + import QualitativeNetworks as QNs + import GraphDynamicalSystems as GDS + + qn = QNs.QN([:x, :y, :z], Union{Symbol, Int, Expr}[:x, :x, :(y - x)], [0:2, 0:2, 0:2]) + + @test length(GDS.vertices(qn)) == 3 + @test length(GDS.edges(qn)) == 4 + @test GDS.update_function(qn) == [:x, :x, :(y - x)] + @test GDS.update_function(qn, [:z]) == [:(y - x)] +end + +@testitem "Convert to SimpleDiGraph" begin + import QualitativeNetworks as QNs + import GraphDynamicalSystems as GDS + import Graphs + + qn = QNs.QN([:x, :y, :z], Union{Symbol, Int, Expr}[:x, :x, :(y - x)], [0:2, 0:2, 0:2]) + graph = GDS.Conversions.to_simple_graph(qn) + + @test Graphs.nv(graph) == 3 + @test Graphs.ne(graph) == 4 +end + +@testitem "Convert to DynamicalSystem" begin + import QualitativeNetworks as QNs + import GraphDynamicalSystems as GDS + import DynamicalSystemsBase as DSB + import MetaGraphsNext as MG + import Graphs + using Attractors: AttractorsViaRecurrences, basins_of_attraction + + function qn_fn_wrapper(v, f, low, high) + return function (state) + previous_val = state[v] + new_val = clamp(previous_val + sign(f(state) - previous_val), low, high) + return round(Int, new_val) + end + end + + vs = [:x, :y, :z] + fns = [ + qn_fn_wrapper(:x, _ -> 2, 0, 2), + qn_fn_wrapper(:y, state -> state[:x], 0, 2), + qn_fn_wrapper(:z, state -> state[:y] - state[:x], 0, 2), + ] + ds = [0:2, 0:2, 0:2] + vd = QNs.VertexData.(fns, zeros(length(vs)), ds) + es = Graphs.Edge.([(1, 1), (1, 2), (1, 3), (2, 3)]) + es_sym = [(:x, :x), (:x, :y), (:x, :z), (:y, :z)] + graph = Graphs.SimpleDiGraph(es) + meta_graph = MG.MetaGraph(graph, vs .=> vd, Tuple.(es_sym) .=> nothing) + qn = QNs.QN(meta_graph) + as = GDS.Conversions.to_arbitrary_steppable(qn) + + @test DSB.current_state(as) == [0, 0, 0] + DSB.step!(as) + @test DSB.current_state(as) == [1, 0, 0] + + grid = Tuple(range(0, 2) for _ in 1:3) + + mapper = AttractorsViaRecurrences(as, grid) + + basins = basins_of_attraction(mapper, grid) + @test length(basins.attractors) == 1 + @test collect(keys(basins.attractors)) == [1] + @test first(basins.attractors[1]) == [2, 2, 0] +end + +@testitem "Convert to discrete system" begin + @test_skip false +end + +@testitem "Explicit imports" begin + using ExplicitImports: test_explicit_imports + test_explicit_imports(QualitativeNetworks) +end diff --git a/implementations/QualitativeNetworks/test/runtests.jl b/implementations/QualitativeNetworks/test/runtests.jl new file mode 100644 index 0000000..f11e9f9 --- /dev/null +++ b/implementations/QualitativeNetworks/test/runtests.jl @@ -0,0 +1,4 @@ +using ReTestItems: runtests, @testitem +import QualitativeNetworks + +runtests(QualitativeNetworks) diff --git a/src/GraphDynamicalSystems.jl b/src/GraphDynamicalSystems.jl index 6f5394e..9db1eca 100644 --- a/src/GraphDynamicalSystems.jl +++ b/src/GraphDynamicalSystems.jl @@ -1,11 +1,36 @@ module GraphDynamicalSystems -using DocStringExtensions +using Compat: @compat -include("gds_interface.jl") +include("interface.jl") -include("qualitative_networks.jl") +using .Interface: schedule_style, domain, update_function, get_state, + set_state!, vertices, edges -# include("io/bma.jl") +@compat public schedule_style, + domain, + update_function, + get_state, + set_state!, + vertices, + edges + +include("schedule.jl") + +using .Schedule: ScheduleStyle, Asynchronous, Synchronous, to_update, determinism, is_deterministic + +@compat public ScheduleStyle, Asynchronous, Synchronous, to_update, determinism, is_deterministic + +include("conversions.jl") + +using .Conversions: Conversions, to_simple_graph, to_arbitrary_steppable + +@compat public Conversions, to_simple_graph, to_arbitrary_steppable + +include("constructors.jl") + +using .Constructors: Constructors, vertex_function_to_edgelist + +@compat public Constructors, vertex_function_to_edgelist end diff --git a/src/constructors.jl b/src/constructors.jl new file mode 100644 index 0000000..e39bce7 --- /dev/null +++ b/src/constructors.jl @@ -0,0 +1,23 @@ +module Constructors +using Compat: @compat + +@compat public vertex_function_to_edgelist + +""" + vertex_function_to_edgelist(vertex_fn_pairs) + vertex_function_to_edgelist(vertex, fn, all_vertices) + +Create a list of edges based on the vertices that appear in `fn`. + +If a vertex `X` has a function `X + Y` and `all_vertices = [X, Y, ...]`, +then return edges `X => X` and `X => Y`. + +!!! note + Requires `AbstractTrees` to be loaded. +""" +function vertex_function_to_edgelist(vertex_fn_pairs) + return vcat(vertex_function_to_edgelist.(first.(vertex_fn_pairs), last.(vertex_fn_pairs), (first.(vertex_fn_pairs),))...) +end +# Second signature implemented in `AbstractTreesExt` + +end diff --git a/src/conversions.jl b/src/conversions.jl new file mode 100644 index 0000000..3fa490d --- /dev/null +++ b/src/conversions.jl @@ -0,0 +1,59 @@ +module Conversions +using Compat: @compat + +@compat public to_simple_graph, to_arbitrary_steppable + +""" + to_simple_graph(gds) + +Convert `gds` to a [`Graphs.SimpleDiGraph`](https://juliagraphs.org/Graphs.jl/dev/core_functions/simplegraphs/#Graphs.SimpleGraphs.SimpleDiGraph). + +`gds` is required to implement the GDS interface. + +!!! tip + Use this method when an object `<:AbstractGraph` is necessary. + The `Graphs` ecosystem currently requires this for most (all?) + of its functionality. Until a trait-based version of the ecosystem + arrives, then one must use this method (or one like it) to construct + something that is `<:AbstractGraph`. + +!!! note + Requires `Graphs` to be loaded. +""" +function to_simple_graph end # implemented in ext/GraphsExt.jl + +""" + to_arbitrary_steppable(gds) + +Convert `gds` to a [`DynamicalSystemsBase.ArbitrarySteppable`](https://juliadynamics.github.io/DynamicalSystemsDocs.jl/dynamicalsystemsbase/stable/#ArbitrarySteppable). + +`gds` is required to implement the GDS interface. + +!!! tip + Use this method when an object `<:DynamicalSystem` is necessary. + The `DynamicalSystems` ecosystem currently requires this for most (all?) + of its functionality. Until a trait-based version of the ecosystem + arrives, then one must use this method (or one like it) to construct + something that is `<:DynamicalSystem`. + +!!! note + Requires `DynamicalSystemsBase` to be loaded. +""" +function to_arbitrary_steppable end # implemented in ext/DynamicalSystemBase.jl + + +""" + to_discrete_system(gds) + +Convert `gds` to a [`AlgebraicDynamics.DWDDynam.DiscreteMachine`](https://algebraicjulia.github.io/AlgebraicDynamics.jl/dev/api/#AlgebraicDynamics.DWDDynam.DiscreteMachine). + +`gds` is required to implement the GDS interface. + +!!! note + Currently unimplemented as a number of dependencies within the + `AlgebraicJulia` ecosystem are not up to date and conflict with those of other + extensions. +""" +function to_discrete_system end + +end diff --git a/src/gds_interface.jl b/src/gds_interface.jl deleted file mode 100644 index 031ff93..0000000 --- a/src/gds_interface.jl +++ /dev/null @@ -1,94 +0,0 @@ -import DynamicalSystemsBase as DSB -import MetaGraphsNext: labels -import Compat: @compat - -@compat public ScheduleStyle, - Asynchronous, - Synchronous, - GraphDynamicalSystem, - GDS, - get_n_entities, - get_schedule, - get_graph, - get_domain, - get_fn - -abstract type ScheduleStyle end -struct Asynchronous <: ScheduleStyle end -struct Synchronous <: ScheduleStyle end - -# A GDS is a DiscreteTimeDynamicalSystem where the model is a MetaGraph with -# functions at each of the vertices in the graph. The parameters of the system -# include the ranges of each of the entities. The state of the system is just -# the current value of each of the entities - -abstract type GraphDynamicalSystem{N, S} <: DSB.DiscreteTimeDynamicalSystem 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 - -abstract type AbstractEntity end - -function get_fn end - -""" - $(TYPEDSIGNATURES) - -Get the state of the GDS. -""" -function DSB.current_state(gds::GDS) - g = get_graph(gds) - l = labels(g) - return DSB.current_state.(getindex.((g,), l)) -end - -function DSB.current_state(gds::GDS, entity) - g = get_graph(gds) - return DSB.current_state(g[entity]) -end - -function get_fn(gds::GDS) - g = get_graph(gds) - l = labels(g) - return get_fn.(getindex.((g,), l)) -end - -function get_domain(gds::GDS) - g = get_graph(gds) - l = labels(g) - return get_domain.(getindex.((g,), l)) -end diff --git a/src/interface.jl b/src/interface.jl new file mode 100644 index 0000000..9d7f99d --- /dev/null +++ b/src/interface.jl @@ -0,0 +1,69 @@ +module Interface +using Compat: @compat + +@compat public schedule_style, + update_function, + domain, + get_state, + set_state!, + vertices, + edges + +""" + schedule_style(gds) + +Get the schedule style of the `gds`. + +Also referred to as the "update scheme". +""" +function schedule_style end + +""" + update_function(gds) + update_function(gds, vertices) + +Get the update function of (all/some) `vertices` of the `gds`. + +Also referred to as "vertex function(s)". +""" +function update_function end + +""" + domain(gds) + domain(gds, vertices) + +Get the domain of (all/some) `vertices` of the `gds`. +""" +function domain end + +""" + get_state(gds) + get_state(gds, vertices) + +Get the state of (all/some) `vertices` of the `gds`. +""" +function get_state end + +""" + set_state!(gds, state) + set_state!(gds, vertex_state_pairs) + +Set the state of the entire `gds` or a subset of vertices' states. +""" +function set_state! end + +""" + vertices(gds) + +Get the vertices of the `gds`. +""" +function vertices end + +""" + edges(gds) + +Get the edges of the `gds`. +""" +function edges end + +end diff --git a/src/qualitative_networks.jl b/src/qualitative_networks.jl deleted file mode 100644 index 2f00547..0000000 --- a/src/qualitative_networks.jl +++ /dev/null @@ -1,171 +0,0 @@ -import MetaGraphsNext as MG -import Graphs -import AbstractTrees as AT -import TermInterface as TI -import DynamicalSystemsBase as DSB -import MLStyle -import SciMLBase -import Compat: @compat - -@compat public QualitativeNetwork, QN, interpret - -""" - $(TYPEDEF) - -A graph dynamical system with a finite domain. State values of each entity are -limited to change by at most 1 per time step. -""" -struct QualitativeNetwork{N_Entities, Schedule, Graph <: MG.MetaGraph} <: - GraphDynamicalSystem{N_Entities, Schedule} - graph::Graph -end - -""" - $(TYPEDEF) - -Alias for a [`QualitativeNetwork`](@ref). -""" -const QN = QualitativeNetwork - -DSB.isinplace(::QualitativeNetwork) = true -DSB.dynamic_rule(qn::QualitativeNetwork) = get_fn(qn) -DSB.current_parameters(qn::QualitativeNetwork) = () -DSB.current_time(::QualitativeNetwork) = 0 -DSB.reinit!(qn::QN, state::AbstractVector) = DSB.set_state!(qn, Int.(state)) - - -mutable struct QNEntity{F, S, D} <: AbstractEntity - fn::F - state::S - domain::D -end - -get_fn(qne::QNEntity) = qne.fn -DSB.current_state(qne::QNEntity) = qne.state -function DSB.set_state!(qne::QNEntity, s) - domain = get_domain(qne) - if s < minimum(domain) || s > maximum(domain) - error( - "New state value for entity must be within its domain (domain: $domain, new state: $s)", - ) - end - return qne.state = s -end -get_domain(qne::QNEntity) = qne.domain - -function QualitativeNetwork(graph::G) where {G <: Graphs.AbstractGraph} - return QualitativeNetwork{Graphs.nv(graph), Synchronous(), G}(graph) -end - -function QualitativeNetwork(update_functions::AbstractDict, domains) - return QualitativeNetwork{Graphs.SimpleDiGraph}(update_functions, domains) -end - -""" - $(TYPEDSIGNATURES) - -Create a [`QualitativeNetwork`](@ref) from the dictionary `update_functions` which should map from entities (`E`) to their functions (`F`) - -The entity names (`E` in the signature) can be anything, while the functions (`F`) are required to either - -- be a numerical constant (like `1`), or a reference to a single entity (like `A`) -- implement the `TermInterface.jl` interface. Any terminal nodes in the functions must be numerical constants or reference an entity. -""" -function QualitativeNetwork{GraphType}( - update_functions::AbstractDict{E, F}, - domains, - )::QualitativeNetwork where {E, F, GraphType <: Graphs.AbstractGraph} - entity_keys = collect(keys(update_functions)) - entity_fns = getindex.((update_functions,), entity_keys) - entity_domains = getindex.((domains,), entity_keys) - get_arguments_or_empty = x -> TI.isexpr(x) ? (x, TI.arguments(x)) : (x, ()) - collect_arguments = - x -> - AT.treemap(get_arguments_or_empty, x) |> - (x -> AT.PreOrderDFS(x -> !(AT.nodevalue(x) in entity_keys), x)) .|> - AT.nodevalue |> - filter(in(entity_keys)) - - referenced_entities = union.(collect_arguments.(entity_fns)) - referenced_indices = - map(ref_for_e -> findfirst.(.==(ref_for_e), (entity_keys,)), referenced_entities) - edges = - Iterators.flatten( - map(((j, idxs),) -> tuple.(idxs, (j,)), enumerate(referenced_indices)), - ) |> collect - graph = GraphType() - Graphs.add_vertices!(graph, length(entity_keys)) - Graphs.add_edge!.((graph,), Graphs.Edge.(edges)) - vertices_description = Pair{E, QNEntity}[ - (e => QNEntity(fn, 0, d)) for - (e, fn, d) in zip(entity_keys, entity_fns, entity_domains) - ] - edges_description = Pair{Tuple{E, E}, Nothing}[ - (entity_keys[s], entity_keys[d]) => nothing for (s, d) in edges - ] - return QualitativeNetwork( - MG.MetaGraph(graph, vertices_description, edges_description, nothing), - ) -end - -function SciMLBase.step!(qn::QualitativeNetwork{N, S}) where {N, S} - return SciMLBase.step!(S, qn) -end -SciMLBase.step!(qn::QN, n::Int, _...) = foreach(_ -> SciMLBase.step!(qn), 1:n) - -function limit_change(next, prev, lower, upper) - return if next > prev - min(upper, prev + 1) - elseif next < prev - max(lower, prev - 1) - else - next - end -end - -function DSB.set_state!( - qn::QN{N, S, M}, - new_state::Int, - entity::L, - ) where {N, S, I, G, L, M <: MG.MetaGraph{I, G, L}} - g = get_graph(qn) - return DSB.set_state!(g[entity], new_state) -end - -function DSB.set_state!(qn::QN, new_state::AbstractVector) - g = get_graph(qn) - - return DSB.set_state!.((qn,), new_state, MG.labels(g)) -end - -function SciMLBase.step!(::Synchronous, qn::QualitativeNetwork) - l = MG.labels(get_graph(qn)) - current_state = DSB.current_state(qn) - current_state_dict = Dict(l .=> current_state) - domains = get_domain(qn) - lower_bounds = minimum.(domains) - upper_bounds = maximum.(domains) - fns = get_fn(qn) - next_state_uncapped = interpret.(fns, (current_state_dict,)) - next_state_capped = - limit_change.(next_state_uncapped, current_state, lower_bounds, upper_bounds) - DSB.set_state!(qn, next_state_capped) - - return qn -end - -function interpret(fn, state) - return MLStyle.@match fn begin - ::Int => fn - ::Symbol => state[fn] - :($a + $b) => interpret(a, state) + interpret(b, state) - :($a - $b) => interpret(a, state) - interpret(b, state) - :($a / $b) => interpret(a, state) / interpret(b, state) - :(min($a, $b)) => min(interpret(a, state), interpret(b, state)) - :(max($a, $b)) => max(interpret(a, state), interpret(b, state)) - :(ceil($a)) => ceil(interpret(a, state)) - :(floor($a)) => floor(interpret(a, state)) - _ => error("Unhandled expression: $fn") - end -end -interpret(fn, qn::QN) = interpret(fn, Dict(entities(qn) .=> DSB.current_state(qn))) diff --git a/src/schedule.jl b/src/schedule.jl new file mode 100644 index 0000000..fd0ce0e --- /dev/null +++ b/src/schedule.jl @@ -0,0 +1,99 @@ +module Schedule +using Compat: @compat +using ..Interface: vertices, schedule_style + +@compat public ScheduleStyle, + Asynchronous, + Synchronous + +""" + ScheduleStyle + +Trait defining the schedule of a GDS. + +A schedule defines which vertices are updated and in which order. +Two basic common schedule types are [`Asynchronous`](@ref) and +[`Synchronous`](@ref). + +A new schedule type `MySchedule<:ScheduleStyle` should implement +`to_update(::MySchedule, gds)` and `determinism(::MySchedule)`. +""" +abstract type ScheduleStyle end + +""" + Determinism + +Trait defining the determinism of a schedule style. +""" +abstract type Determinism end +struct Deterministic <: Determinism end +struct NonDeterministic <: Determinism end + +""" + is_deterministic(gds) + +Whether the `gds` has a deterministic schedule. +""" +function is_deterministic(gds) + return is_deterministic(schedule_style(gds)) +end + +""" + is_deterministic(s::ScheduleStyle) + +Whether schedule `s` is deterministic. +""" +function is_deterministic(s::ScheduleStyle) + return is_deterministic(determinism(s)) +end + +function is_deterministic(::Deterministic) + return true +end + +function is_deterministic(::NonDeterministic) + return false +end + +""" + to_update(gds) + +Get the vertices of the `gds` to update at the current timestep. + +Implement `to_update(::ScheduleStyle, gds)` for new schedule types. +""" +function to_update(gds) + return to_update(schedule_style(gds), gds) +end + +""" + Asynchronous + +An asynchronous schedule randomly chooses a vertex to update at each timestep. +""" +struct Asynchronous <: ScheduleStyle end + +function to_update(::Asynchronous, gds) + return rand(collect(vertices(gds)), 1) +end + +function determinism(::Asynchronous) + return NonDeterministic() +end + +""" + Synchronous + +A synchronous schedule updates _all_ vertices at each timestep. +""" +struct Synchronous <: ScheduleStyle end + +function to_update(::Synchronous, gds) + return vertices(gds) +end + +function determinism(::Synchronous) + return Deterministic() +end + +end diff --git a/test/Project.toml b/test/Project.toml index cd7bb91..d761fd4 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,13 +1,9 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -Attractors = "f3fd9213-ca85-4dba-9dfd-7fc91308fec7" -DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4" +BooleanNetworks = "97f9906b-beee-4f75-a06a-7a05f392a141" +ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" GraphDynamicalSystems = "13529e2e-ed53-56b1-bd6f-420b01fca819" -Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" -IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" -MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +QualitativeNetworks = "ba61d19f-daf7-42ed-91ab-e8e04fc92a6d" ReTestItems = "817f1d60-ba6b-4fd5-9520-3cf149f6a823" -SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/qn_test.jl b/test/qn_test.jl deleted file mode 100644 index f2b4b9a..0000000 --- a/test/qn_test.jl +++ /dev/null @@ -1,141 +0,0 @@ -@testsetup module RandomSetup -using Random: seed! -seed!(42) -end - -@testsetup module ExampleQN -export qn_size, qn_fns, qn_domains, qn, qn_entities -import GraphDynamicalSystems as GDS - -qn_size = 3 -qn_entities = [:X, :Y, :Z] -qn_fns = Dict(:X => 1, :Y => :X, :Z => :(Y - X)) -qn_domains = Dict(e => 0:5 for e in qn_entities) -qn = GDS.QN(qn_fns, qn_domains) -end - -@testitem "From a dictionary of functions" begin - import Graphs - import SciMLBase - import GraphDynamicalSystems as GDS - import DynamicalSystemsBase as DSB - - fns = Dict(:A => 1, :B => :A, :C => :(B - A)) - d = Dict(k => 0:3 for k in keys(fns)) - qn = GDS.QualitativeNetwork{Graphs.SimpleGraph}(fns, d) - SciMLBase.step!(qn) - @test DSB.current_state(qn) == [1, 0, 0] - DSB.set_state!(qn, [1, 2, 3]) - @test DSB.current_state(qn) == [1, 2, 3] - SciMLBase.step!(qn) - @test DSB.current_state(qn) == [1, 1, 2] - SciMLBase.step!(qn) - @test DSB.current_state(qn) == [1, 1, 1] - SciMLBase.step!(qn) - @test DSB.current_state(qn) == [1, 1, 0] -end - -@testitem "QN Graph Correctness" begin - import GraphDynamicalSystems: QN, get_graph - import MetaGraphsNext: edge_labels - - target_fns = Dict(:a => :(-c), :b => :a, :c => :b) - domains = Dict(:a => 0:2, :b => 0:2, :c => 0:2) - - qn = QN(target_fns, domains) - g = get_graph(qn) - - @test haskey(g, :c, :a) - @test haskey(g, :a, :b) - @test haskey(g, :b, :c) -end - -@testitem "QN properties, fields" setup = [RandomSetup, ExampleQN] begin - import GraphDynamicalSystems: entities, get_fn, get_domain - using DynamicalSystemsBase: - current_state, - set_state!, - step!, - isinplace, - dynamic_rule, - current_parameters, - current_time - set_state!(qn, 1, :X) - - @test length(entities(qn)) == qn_size - - @test length(get_fn(qn)) == qn_size - - @test all(current_state(qn) .<= maximum.(get_domain(qn))) - - @test current_state(qn, :X) == 1 - - @test_throws r"domain" set_state!(qn, 6, :X) - @test isinplace(qn) - @test dynamic_rule(qn) == get_fn(qn) - @test current_parameters(qn) == () - @test current_time(qn) == 0 -end - -@testitem "Target Function" setup = [RandomSetup, ExampleQN] begin - import GraphDynamicalSystems: interpret - using DynamicalSystemsBase: set_state! - set_state!(qn, 1, :X) - set_state!(qn, 1, :Y) - set_state!(qn, 1, :Z) - - # 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(:(X + Y), qn) == 2 - @test interpret(:(X - Y), qn) == 0 - set_state!(qn, 2, :Y) - @test interpret(:(X / Y), qn) == 0.5 - @test interpret(:(X / 2), qn) == 0.5 - @test interpret(:(min(X, Y)), qn) == 1 - @test interpret(:(max(X, Y)), qn) == 2 - @test interpret(:(ceil(X / Y)), qn) == 1 - @test interpret(:(floor(X / Y)), qn) == 0 - @test_throws r"Unhandled" interpret(:(nonexistent_function(A)), qn) -end - -@testitem "Async QN" setup = [RandomSetup, ExampleQN] begin - using DynamicalSystemsBase: step!, current_state, set_state! - import GraphDynamicalSystems: Asynchronous, QN - qn_size = 3 - max_eq_depth = 3 - - for N in 2:5 # a few different levels of N - for _ in 1:100 # 100 different initializations - domains = Dict(e => 0:N for e in qn_entities) - async_qn = QN(qn_fns, domains) - step!(async_qn, 100) - @test all(current_state(async_qn) .<= maximum.(values(domains))) - end - end - -end - -@testitem "Get attractors" setup = [RandomSetup, ExampleQN] begin - import GraphDynamicalSystems: Asynchronous - using Attractors: AttractorsViaRecurrences, basins_of_attraction - - grid = Tuple(range(0, 1) for _ in 1:qn_size) - - mapper = AttractorsViaRecurrences(qn, grid) - - basins = basins_of_attraction(mapper, grid) -end - -@testitem "Constructor with entity labels that are expressions" begin - import GraphDynamicalSystems as GDS - import Graphs: ne, nv - - qn_entities = [:(var(1)), :(var(2)), :(var(3))] - qn_fns = Dict(:(var(1)) => 1, :(var(2)) => :(var(1)), :(var(3)) => :(var(2) - var(1))) - qn_domains = Dict(e => 0:5 for e in qn_entities) - qn = GDS.QN(qn_fns, qn_domains) - graph = GDS.get_graph(qn) - @test ne(graph) == 3 - @test nv(graph) == 3 -end diff --git a/test/quality_tests.jl b/test/quality_tests.jl index 21df1e7..27ea7cc 100644 --- a/test/quality_tests.jl +++ b/test/quality_tests.jl @@ -3,9 +3,12 @@ Aqua.test_all(GraphDynamicalSystems) end -@testitem "Code linting (JET.jl)" begin +@testitem "Code quality (JET.jl)" begin using JET - if VERSION ≥ v"1.12" - JET.test_package(GraphDynamicalSystems; target_modules = (GraphDynamicalSystems,)) - end + JET.test_package(GraphDynamicalSystems; target_modules = (GraphDynamicalSystems,)) +end + +@testitem "Explicit imports" begin + using ExplicitImports: test_explicit_imports + test_explicit_imports(GraphDynamicalSystems) end diff --git a/test/quick.jl b/test/quick.jl deleted file mode 100644 index 455cba4..0000000 --- a/test/quick.jl +++ /dev/null @@ -1,12 +0,0 @@ -# -# `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() -rt() = runtests(GraphDynamicalSystems, failfast = true, failures_first = true) diff --git a/test/runtests.jl b/test/runtests.jl index a9f9a8c..9c1d52a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,6 @@ -using ReTestItems, GraphDynamicalSystems +using ReTestItems: runtests, @testitem +import GraphDynamicalSystems, BooleanNetworks, QualitativeNetworks +runtests(BooleanNetworks) +runtests(QualitativeNetworks) runtests(GraphDynamicalSystems)