diff --git a/src/Topologies/Topologies.jl b/src/Topologies/Topologies.jl index ef13617a0..fbc58a74f 100644 --- a/src/Topologies/Topologies.jl +++ b/src/Topologies/Topologies.jl @@ -68,27 +68,26 @@ struct Topology end export Topology -# Wrap an absolute node type. +# Wrap an absolute node index. struct Abs i::Int end -# Wrap a relative node type. +# Wrap a relative node index. struct Rel i::Int end -# Either an index or a label. -const AbsRef = Union{Abs,Symbol} -const RelRef = Union{Rel,Symbol} - # When exposing indices # explicit whether they mean relative or absolute. const IRef = Union{Int,Symbol} +const RelRef = Union{Rel,Symbol} relative(i::Int) = Rel(i) absolute(i::Int) = Abs(i) relative(lab::Symbol) = lab absolute(lab::Symbol) = lab +# A combination of Relative + Node type info constitutes an absolute node ref. +const AbsRef = Union{Abs,Symbol,Tuple{RelRef,IRef}} include("unchecked_queries.jl") const U = Unchecked # Ease refs to unchecked queries. @@ -101,30 +100,30 @@ include("display.jl") # Construction primitives. # Only push whole slices of nodes of a new type at once. -function add_nodes!(top::Topology, labels, type::Symbol) +function add_nodes!(g::Topology, labels, type::Symbol) # Check whole transaction before commiting. - has_node_type(top, type) && + has_node_type(g, type) && argerr("Node type $(repr(type)) already exists in the topology.") - has_edge_type(top, type) && + has_edge_type(g, type) && argerr("Node type $(repr(type)) would be confused with edge type $(repr(type)).") - labels = check_new_nodes_labels(top, labels) + labels = check_new_nodes_labels(g, labels) # Add new node type. - push!(top.node_types_labels, type) - top.node_types_index[type] = length(top.node_types_labels) + push!(g.node_types_labels, type) + g.node_types_index[type] = length(g.node_types_labels) # Add new associated nodes. - nindex = top.nodes_index - nlabs = top.nodes_labels + nindex = g.nodes_index + nlabs = g.nodes_labels n_before = length(nlabs) for new_lab in labels push!(nlabs, new_lab) nindex[new_lab] = length(nlabs) - for adj in (top.outgoing, top.incoming) + for adj in (g.outgoing, g.incoming) # Need an entry for every edge type. entry = Vector{OrderedSet{Int}}() - for _ in 1:n_edge_types(top) + for _ in 1:n_edge_types(g) push!(entry, OrderedSet()) end push!(adj, entry) @@ -133,57 +132,57 @@ function add_nodes!(top::Topology, labels, type::Symbol) # Update value. n_after = length(nlabs) - push!(top.nodes_types, n_before+1:n_after) - push!(top.n_nodes, n_after - n_before) + push!(g.nodes_types, n_before+1:n_after) + push!(g.n_nodes, n_after - n_before) - top + g end export add_nodes! -function add_edge_type!(top::Topology, type::Symbol) +function add_edge_type!(g::Topology, type::Symbol) # Check transaction. - haskey(top.edge_types_index, type) && + haskey(g.edge_types_index, type) && argerr("Edge type $(repr(type)) already exists in the topology.") - haskey(top.node_types_index, type) && + haskey(g.node_types_index, type) && argerr("Edge type $(repr(type)) would be confused with node type $(repr(type)).") # Commit. - push!(top.edge_types_labels, type) - top.edge_types_index[type] = length(top.edge_types_labels) - for adj in (top.outgoing, top.incoming) + push!(g.edge_types_labels, type) + g.edge_types_index[type] = length(g.edge_types_labels) + for adj in (g.outgoing, g.incoming) for node in adj node isa Tombstone && continue push!(node, OrderedSet{Int}()) end end - push!(top.n_edges, 0) + push!(g.n_edges, 0) - top + g end export add_edge_type! -function add_edge!(top::Topology, type::IRef, source::AbsRef, target::AbsRef) +function add_edge!(g::Topology, type::IRef, source::AbsRef, target::AbsRef) # Check transaction. - check_edge_type(top, type) - check_node_ref(top, source) - check_node_ref(top, target) - i_type = U.edge_type_index(top, type) - i_source = U.node_abs_index(top, source) - i_target = U.node_abs_index(top, target) - check_live_node(top, i_source, source) - check_live_node(top, i_target, target) - U.has_edge(top, i_type, i_source, i_target) && + check_edge_type(g, type) + check_node_ref(g, source) + check_node_ref(g, target) + i_type = U.edge_type_index(g, type) + i_source = U.node_abs_index(g, source) + i_target = U.node_abs_index(g, target) + check_live_node(g, i_source, source) + check_live_node(g, i_target, target) + U.has_edge(g, i_type, i_source, i_target) && argerr("There is already an edge of type $(repr(type)) \ between nodes $(repr(source)) and $(repr(target)).") # Commit. - _add_edge!(top, i_type, i_source, i_target) + _add_edge!(g, i_type, i_source, i_target) end -function _add_edge!(top::Topology, i_type::Int, i_source::Abs, i_target::Abs) - push!(top.outgoing[i_source.i][i_type], i_target.i) - push!(top.incoming[i_target.i][i_type], i_source.i) - top.n_edges[i_type] += 1 - top +function _add_edge!(g::Topology, i_type::Int, i_source::Abs, i_target::Abs) + push!(g.outgoing[i_source.i][i_type], i_target.i) + push!(g.incoming[i_target.i][i_type], i_source.i) + g.n_edges[i_type] += 1 + g end export add_edge! @@ -193,37 +192,37 @@ include("./edges_from_matrices.jl") # Remove all neighbours of this node and replace it with a tombstone. # The exposed version is checked. -function remove_node!(top::Topology, node::RelRef, type::IRef) +function remove_node!(g::Topology, node::RelRef, type::IRef) # Check transaction. - check_node_type(top, type) - check_node_ref(top, node, type) - i_node = U.node_abs_index(top, node, type) - U.is_live(top, i_node) || alreadyerr(node) - i_type = U.node_type_index(top, type) - _remove_node!(top, i_node, i_type) + check_node_type(g, type) + check_node_ref(g, node, type) + i_node = U.node_abs_index(g, (node, type)) + U.is_live(g, i_node) || alreadyerr(node) + i_type = U.node_type_index(g, type) + _remove_node!(g, i_node, i_type) end # Not specifying the type requires a linear search for it. -function remove_node!(top::Topology, node::AbsRef) +function remove_node!(g::Topology, node::AbsRef) # Check transaction. - check_node_ref(top, node) - i_node = U.node_abs_index(top, node) - U.is_live(top, i_node) || alreadyerr(node) - i_type = U.type_index_of_node(top, node) - _remove_node!(top, i_node, i_type) + check_node_ref(g, node) + i_node = U.node_abs_index(g, node) + U.is_live(g, i_node) || alreadyerr(node) + i_type = U.type_index_of_node(g, node) + _remove_node!(g, i_node, i_type) end alreadyerr(node) = argerr("Node $(repr(node)) was already removed from this topology.") # Commit. -function _remove_node!(top::Topology, i_node::Abs, i_type::Int) +function _remove_node!(g::Topology, i_node::Abs, i_type::Int) # Assumes the node is valid and live, and that the type does correspond. - top.n_edges .-= length.(top.outgoing[i_node.i]) - top.n_edges .-= length.(top.incoming[i_node.i]) + g.n_edges .-= length.(g.outgoing[i_node.i]) + g.n_edges .-= length.(g.incoming[i_node.i]) ts = Tombstone() - top.outgoing[i_node.i] = ts - top.incoming[i_node.i] = ts - for adjacency in (top.outgoing, top.incoming) + g.outgoing[i_node.i] = ts + g.incoming[i_node.i] = ts + for adjacency in (g.outgoing, g.incoming) for other in adjacency other isa Tombstone && continue for neighbours in other @@ -231,29 +230,29 @@ function _remove_node!(top::Topology, i_node::Abs, i_type::Int) end end end - top.n_nodes[i_type] -= 1 - top + g.n_nodes[i_type] -= 1 + g end export remove_node! #------------------------------------------------------------------------------------------- """ - disconnected_components(top::Topology) + disconnected_components(g::Topology) Iterate over the disconnected component within the topology. This create a collection of topologies with all the same compartments and nodes indices, but with different nodes marked as removed to constitute the various components. """ -function disconnected_components(top::Topology) +function disconnected_components(g::Topology) # Construct a simpler graph representation # with all nodes and edges compartments pooled together. graph = SimpleDiGraph() - for _ in 1:length(top.nodes_labels) + for _ in 1:length(g.nodes_labels) add_vertex!(graph) end - for (i_src, et) in enumerate(top.outgoing) + for (i_src, et) in enumerate(g.outgoing) et isa Tombstone && continue for targets in et, i_tgt in targets Graphs.add_edge!(graph, i_src, i_tgt) @@ -264,29 +263,26 @@ function disconnected_components(top::Topology) Iterators.filter(weakly_connected_components(graph)) do component_nodes # Removed nodes result in degenerated singleton components. # Dismiss them. - !( - length(component_nodes) == 1 && - U.is_removed(top, Abs(first(component_nodes))) - ) + !(length(component_nodes) == 1 && U.is_removed(g, Abs(first(component_nodes)))) end, ) do component_nodes # Construct a whole new value with only these nodes remaining. new = Topology() # All types are copied as-is. - append!(new.node_types_labels, top.node_types_labels) - append!(new.edge_types_labels, top.edge_types_labels) - for (k, v) in top.node_types_index + append!(new.node_types_labels, g.node_types_labels) + append!(new.edge_types_labels, g.edge_types_labels) + for (k, v) in g.node_types_index new.node_types_index[k] = v push!(new.n_nodes, 0) end - for (k, v) in top.edge_types_index + for (k, v) in g.edge_types_index new.edge_types_index[k] = v push!(new.n_edges, 0) end # All nodes are copied as-is. - append!(new.nodes_labels, top.nodes_labels) - append!(new.nodes_types, top.nodes_types) - for (k, v) in top.nodes_index + append!(new.nodes_labels, g.nodes_labels) + append!(new.nodes_types, g.nodes_types) + for (k, v) in g.nodes_index new.nodes_index[k] = v end # But only the ones in this component are reinserted with their neighbours, @@ -298,8 +294,8 @@ function disconnected_components(top::Topology) if i_node > last(new.nodes_types[i_node_type]) i_node_type += 1 end - inc = top.incoming[i_node] - out = top.outgoing[i_node] + inc = g.incoming[i_node] + out = g.outgoing[i_node] if i_node in component_nodes && !(out isa Tombstone) new_in = Vector{OrderedSet{Int}}() new_out = Vector{OrderedSet{Int}}() diff --git a/src/Topologies/checks.jl b/src/Topologies/checks.jl index 0d454272b..fa4c69eb4 100644 --- a/src/Topologies/checks.jl +++ b/src/Topologies/checks.jl @@ -11,25 +11,24 @@ function check_index(i, n, what) i end -has_node_type(top::Topology, i::Int) = has_index(i, length(top.node_types_labels)) -check_node_type(top::Topology, i::Int) = - check_index(i, length(top.node_types_labels), "node type") +has_node_type(g::Topology, i::Int) = has_index(i, length(g.node_types_labels)) +check_node_type(g::Topology, i::Int) = + check_index(i, length(g.node_types_labels), "node type") -has_edge_type(top::Topology, i::Int) = has_index(i, length(top.edge_types_labels)) -check_edge_type(top::Topology, i::Int) = - check_index(i, length(top.edge_types_labels), "edge type") +has_edge_type(g::Topology, i::Int) = has_index(i, length(g.edge_types_labels)) +check_edge_type(g::Topology, i::Int) = + check_index(i, length(g.edge_types_labels), "edge type") -has_node_ref(top::Topology, abs::Abs) = has_index(abs.i, length(top.nodes_labels)) -check_node_ref(top::Topology, abs::Abs) = - check_index(abs.i, length(top.nodes_labels), "node") +has_node_ref(g::Topology, abs::Abs) = has_index(abs.i, length(g.nodes_labels)) +check_node_ref(g::Topology, abs::Abs) = check_index(abs.i, length(g.nodes_labels), "node") # Check relative indices ASSUMING the node type is valid. -has_node_ref(top::Topology, rel::Rel, type::IRef) = - has_index(rel.i, U.n_nodes_including_removed(top, type)) -check_node_ref(top::Topology, rel::Rel, type::IRef) = check_index( +has_node_ref(g::Topology, rel::Rel, type::IRef) = + has_index(rel.i, U.n_nodes_including_removed(g, type)) +check_node_ref(g::Topology, rel::Rel, type::IRef) = check_index( rel.i, - U.n_nodes_including_removed(top, type), - () -> "$(repr(U.node_type_label(top, type))) node", + U.n_nodes_including_removed(g, type), + () -> "$(repr(U.node_type_label(g, type))) node", ) #------------------------------------------------------------------------------------------- @@ -49,56 +48,53 @@ function check_label(lab, set, what) lab end -has_node_type(top::Topology, lab::Symbol) = - has_label(lab, keys(top.node_types_index)) -check_node_type(top::Topology, lab::Symbol) = - check_label(lab, keys(top.node_types_index), "node type") +has_node_type(g::Topology, lab::Symbol) = has_label(lab, keys(g.node_types_index)) +check_node_type(g::Topology, lab::Symbol) = + check_label(lab, keys(g.node_types_index), "node type") -has_edge_type(top::Topology, lab::Symbol) = - has_label(lab, keys(top.edge_types_index)) -check_edge_type(top::Topology, lab::Symbol) = - check_label(lab, keys(top.edge_types_index), "edge type") +has_edge_type(g::Topology, lab::Symbol) = has_label(lab, keys(g.edge_types_index)) +check_edge_type(g::Topology, lab::Symbol) = + check_label(lab, keys(g.edge_types_index), "edge type") -has_node_ref(top::Topology, lab::Symbol) = has_label(lab, keys(top.nodes_index)) -check_node_ref(top::Topology, lab::Symbol) = check_label(lab, keys(top.nodes_index), "node") +has_node_ref(g::Topology, lab::Symbol) = has_label(lab, keys(g.nodes_index)) +check_node_ref(g::Topology, lab::Symbol) = check_label(lab, keys(g.nodes_index), "node") # Check "relative labels" ASSUMING the node type is valid. -has_node_ref(top::Topology, lab::Symbol, type::IRef) = - has_label(lab, U._node_labels(top, type)) -check_node_ref(top::Topology, lab::Symbol, type::IRef) = check_label( +has_node_ref(g::Topology, lab::Symbol, type::IRef) = has_label(lab, U._node_labels(g, type)) +check_node_ref(g::Topology, lab::Symbol, type::IRef) = check_label( lab, - U._nodes_labels(top, type), - () -> "$(repr(U.node_type_label(top, type))) node", + U._nodes_labels(g, type), + () -> "$(repr(U.node_type_label(g, type))) node", ) #------------------------------------------------------------------------------------------- # Check node liveliness, assuming the reference is valid. -function check_live_node(top::Topology, node::AbsRef, original_ref::AbsRef = node) +function check_live_node(g::Topology, node::AbsRef, original_ref::AbsRef = node) # (use the original reference to trace back to actual user input # and improve error message) - U.is_removed(top, node) && + U.is_removed(g, node) && argerr("Node $(repr(original_ref)) has been removed from this topology.") node end #------------------------------------------------------------------------------------------- # Check node labels availability. -function check_new_nodes_labels(top::Topology, labels::Vector{Symbol}) +function check_new_nodes_labels(g::Topology, labels::Vector{Symbol}) for new_lab in labels - if has_node_ref(top, new_lab) + if has_node_ref(g, new_lab) argerr("Label :$new_lab was already given \ to a node of type \ - $(repr(U.type_of_node(top, new_lab))).") + $(repr(U.type_of_node(g, new_lab))).") end end labels end -function check_new_nodes_labels(top::Topology, labels) +function check_new_nodes_labels(g::Topology, labels) try labels = Symbol[Symbol(l) for l in labels] catch argerr("The labels provided cannot be iterated into a collection of symbols. \ Received: $(repr(labels)).") end - check_new_nodes_labels(top, labels) + check_new_nodes_labels(g, labels) end diff --git a/src/Topologies/display.jl b/src/Topologies/display.jl index 8d8fb825e..bd7c2ea86 100644 --- a/src/Topologies/display.jl +++ b/src/Topologies/display.jl @@ -10,11 +10,11 @@ function _th(n) end th(n) = "$n$(_th(n))" -function Base.show(io::IO, top::Topology) - n_nt = n_node_types(top) - n_et = n_edge_types(top) - n_n = sum((U.n_nodes(top, i) for i in 1:n_nt); init = 0) - n_e = sum((U.n_edges(top, i) for i in 1:n_et); init = 0) +function Base.show(io::IO, g::Topology) + n_nt = n_node_types(g) + n_et = n_edge_types(g) + n_n = sum((U.n_nodes(g, i) for i in 1:n_nt); init = 0) + n_e = sum((U.n_edges(g, i) for i in 1:n_et); init = 0) print( io, "Topology(\ @@ -26,11 +26,11 @@ function Base.show(io::IO, top::Topology) ) end -function Base.show(io::IO, ::MIME"text/plain", top::Topology) - n_nt = n_node_types(top) - n_et = n_edge_types(top) - n_n = sum((U.n_nodes(top, i) for i in 1:n_nt); init = 0) - n_e = sum((U.n_edges(top, i) for i in 1:n_et); init = 0) +function Base.show(io::IO, ::MIME"text/plain", g::Topology) + n_nt = n_node_types(g) + n_et = n_edge_types(g) + n_n = sum((U.n_nodes(g, i) for i in 1:n_nt); init = 0) + n_e = sum((U.n_edges(g, i) for i in 1:n_et); init = 0) print( io, "Topology for $n_nt node type$(s(n_nt)) \ @@ -43,14 +43,14 @@ function Base.show(io::IO, ::MIME"text/plain", top::Topology) print(".") end println(io, "\n Nodes:") - for (i_type, type) in enumerate(_node_types(top)) + for (i_type, type) in enumerate(_node_types(g)) i_type > 1 && println(io) tomb = Symbol[] # Collect removed nodes to display at the end. print(io, " $(repr(type)) => [") empty = true - for i_node in U.nodes_abs_indices(top, i_type) - node = U.node_label(top, i_node) - if U.is_removed(top, i_node) + for i_node in U.nodes_abs_indices(g, i_type) + node = U.node_label(g, i_node) + if U.is_removed(g, i_node) push!(tomb, node) continue end @@ -64,16 +64,16 @@ function Base.show(io::IO, ::MIME"text/plain", top::Topology) end end n_e > 0 && print(io, "\n Edges:") - for (i_type, type) in enumerate(_edge_types(top)) + for (i_type, type) in enumerate(_edge_types(g)) print(io, "\n $(repr(type))") empty = true - for (i_source, _neighbours) in U._outgoing_edges_indices(top, i_type) + for (i_source, _neighbours) in U._outgoing_edges_indices(g, i_type) isempty(_neighbours) && continue - source = U.node_label(top, Abs(i_source)) + source = U.node_label(g, Abs(i_source)) print(io, "\n $(repr(source)) => [") first = true for i_target in _neighbours - target = U.node_label(top, Abs(i_target)) + target = U.node_label(g, Abs(i_target)) first || print(io, ", ") print(io, repr(target)) first = false @@ -88,9 +88,9 @@ function Base.show(io::IO, ::MIME"text/plain", top::Topology) end # A debug display to just screen through the whole value. -debug(top::Topology) = +debug(g::Topology) = for fn in fieldnames(Topology) - val = getfield(top, fn) + val = getfield(g, fn) if fn in (:incoming, :outgoing) println("$fn: ") for (i_adj, adj) in enumerate(val) diff --git a/src/Topologies/queries.jl b/src/Topologies/queries.jl index b6febe707..d191d844c 100644 --- a/src/Topologies/queries.jl +++ b/src/Topologies/queries.jl @@ -5,18 +5,18 @@ const imap = Iterators.map idmap(x) = imap(identity, x) # Useful to not leak refs to private collections. # Information about types. -n_node_types(top::Topology) = length(top.node_types_labels) -n_edge_types(top::Topology) = length(top.edge_types_labels) +n_node_types(g::Topology) = length(g.node_types_labels) +n_edge_types(g::Topology) = length(g.edge_types_labels) export n_node_types, n_edge_types -_node_types(top::Topology) = top.node_types_labels -_edge_types(top::Topology) = top.edge_types_labels -node_types(top::Topology) = idmap(_node_types(top)) -edge_types(top::Topology) = idmap(_edge_types(top)) +_node_types(g::Topology) = g.node_types_labels +_edge_types(g::Topology) = g.edge_types_labels +node_types(g::Topology) = idmap(_node_types(g)) +edge_types(g::Topology) = idmap(_edge_types(g)) export node_types, edge_types -is_node_type(top::Topology, i::Int) = 1 <= i <= length(top.node_types_labels) -is_edge_type(top::Topology, i::Int) = 1 <= i <= length(top.edge_types_labels) -is_node_type(top::Topology, lab::Symbol) = lab in keys(top.node_types_index) -is_edge_type(top::Topology, lab::Symbol) = lab in keys(top.edge_types_index) +is_node_type(g::Topology, i::Int) = 1 <= i <= length(g.node_types_labels) +is_edge_type(g::Topology, i::Int) = 1 <= i <= length(g.edge_types_labels) +is_node_type(g::Topology, lab::Symbol) = lab in keys(g.node_types_index) +is_edge_type(g::Topology, lab::Symbol) = lab in keys(g.edge_types_index) export is_node_type, is_edge_type diff --git a/src/Topologies/unchecked_queries.jl b/src/Topologies/unchecked_queries.jl index 7e5fec74d..51d9a0c67 100644 --- a/src/Topologies/unchecked_queries.jl +++ b/src/Topologies/unchecked_queries.jl @@ -3,7 +3,7 @@ # Methods that would leak references are protected with a '_' prefix. module Unchecked -import ..Topologies: Topology, Tombstone, Abs, Rel, AbsRef, IRef +import ..Topologies: Topology, Tombstone, Abs, Rel, AbsRef, RelRef, IRef const imap = Iterators.map const ifilter = Iterators.filter @@ -12,12 +12,12 @@ idmap(x) = imap(identity, x) # Useful to not leak refs to private collections. # ========================================================================================== # Types. -node_type_label(top::Topology, i::Int) = top.node_types_labels[i] -node_type_index(top::Topology, lab::Symbol) = top.node_types_index[lab] +node_type_label(g::Topology, i::Int) = g.node_types_labels[i] +node_type_index(g::Topology, lab::Symbol) = g.node_types_index[lab] node_type_label(::Topology, lab::Symbol) = lab node_type_index(::Topology, i::Int) = i -edge_type_label(top::Topology, i::Int) = top.edge_types_labels[i] -edge_type_index(top::Topology, lab::Symbol) = top.edge_types_index[lab] +edge_type_label(g::Topology, i::Int) = g.edge_types_labels[i] +edge_type_index(g::Topology, lab::Symbol) = g.edge_types_index[lab] edge_type_label(::Topology, lab::Symbol) = lab edge_type_index(::Topology, i::Int) = i @@ -25,176 +25,180 @@ edge_type_index(::Topology, i::Int) = i # Nodes. # General information. -n_nodes(top::Topology, type::IRef) = top.n_nodes[node_type_index(top, type)] -n_nodes_including_removed(top::Topology, type::IRef) = - length(top.nodes_types[node_type_index(top, type)]) -_nodes_abs_range(top::Topology, type::IRef) = # Okay to leak (immutable) but not abs-wrapped.. - top.nodes_types[node_type_index(top, type)] -nodes_abs_indices(top::Topology, type::IRef) = - imap(Abs, top.nodes_types[node_type_index(top, type)]) -_nodes_labels(top::Topology, type::IRef) = top.nodes_labels[_nodes_abs_range(top, type)] -node_labels(top::Topology, type::IRef) = idmap(_nodes_labels(top, type)) +n_nodes(g::Topology, type::IRef) = g.n_nodes[node_type_index(g, type)] +n_nodes_including_removed(g::Topology, type::IRef) = + length(g.nodes_types[node_type_index(g, type)]) +_nodes_abs_range(g::Topology, type::IRef) = # Okay to leak (immutable) but not abs-wrapped.. + g.nodes_types[node_type_index(g, type)] +nodes_abs_indices(g::Topology, type::IRef) = + imap(Abs, g.nodes_types[node_type_index(g, type)]) +_nodes_labels(g::Topology, type::IRef) = g.nodes_labels[_nodes_abs_range(g, type)] +node_labels(g::Topology, type::IRef) = idmap(_nodes_labels(g, type)) # Particular information about nodes. -node_label(top::Topology, abs::Abs) = top.nodes_labels[abs.i] -node_abs_index(top::Topology, label::Symbol) = Abs(top.nodes_index[label]) +node_label(g::Topology, abs::Abs) = g.nodes_labels[abs.i] node_label(::Topology, lab::Symbol) = lab +node_label(g::Topology, (rel, type)::Tuple{RelRef,IRef}) = + node_label(g, node_abs_index(g, rel, type)) +node_abs_index(g::Topology, label::Symbol) = Abs(g.nodes_index[label]) node_abs_index(::Topology, abs::Abs) = abs # Append correct offset to convert between relative / absolute indices. -first_node_abs_index(top::Topology, type::IRef) = first(nodes_abs_indices(top, type)) -node_index_offset(top::Topology, type::IRef) = first_node_abs_index(top, type).i - 1 -node_abs_index(top::Topology, relative_index::Rel, type::IRef) = - Abs(relative_index.i + node_index_offset(top, type)) -node_rel_index(top::Topology, node::AbsRef, type::IRef) = - Rel(node_abs_index(top, node).i - node_index_offset(top, type)) -# For consistency with the above, node type information is ignored when unnecessary -# because we ASSUME here that it has already been checked for consistency. -node_abs_index(top::Topology, lab::AbsRef, ::IRef) = node_abs_index(top, lab) +first_node_abs_index(g::Topology, type::IRef) = first(nodes_abs_indices(g, type)) +node_index_offset(g::Topology, type::IRef) = first_node_abs_index(g, type).i - 1 +node_abs_index(g::Topology, relative_index::Rel, type::IRef) = + Abs(relative_index.i + node_index_offset(g, type)) +node_rel_index(g::Topology, node::AbsRef, type::IRef) = + Rel(node_abs_index(g, node).i - node_index_offset(g, type)) +node_abs_index(g::Topology, (rel, type)::Tuple{Rel,IRef}) = node_abs_index(g, rel, type) +# For consistency, ignore the node type if not useful, ASSUMING it has been checked. +node_abs_index(g::Topology, (lab, _)::Tuple{Symbol,IRef}) = node_abs_index(g, lab) # Querying node type requires a linear search, # but it is generally assumed that if you know the node, then you already know its type. -type_index_of_node(top::Topology, node::AbsRef) = - findfirst(range -> node_abs_index(top, node).i in range, top.nodes_types) -type_of_node(top::Topology, node::AbsRef) = - node_type_label(top, type_index_of_node(top, node)) +type_index_of_node(g::Topology, node::AbsRef) = + findfirst(range -> node_abs_index(g, node).i in range, g.nodes_types) +type_of_node(g::Topology, node::AbsRef) = node_type_label(g, type_index_of_node(g, node)) # But it is O(1) to check whether a given node is of the given type. -function is_node_of_type(top::Topology, node::AbsRef, type::IRef) - i_type = node_type_index(top, type) - i_node = node_abs_index(top, node) - i_node.i in top.nodes_types[i_type] +function is_node_of_type(g::Topology, node::AbsRef, type::IRef) + i_type = node_type_index(g, type) + i_node = node_abs_index(g, node) + i_node.i in g.nodes_types[i_type] end -is_removed(top::Topology, node::AbsRef) = - top.outgoing[node_abs_index(top, node).i] isa Tombstone -is_live(top::Topology, node::AbsRef) = !is_removed(top, node) +is_removed(g::Topology, node::AbsRef) = g.outgoing[node_abs_index(g, node).i] isa Tombstone +is_live(g::Topology, node::AbsRef) = !is_removed(g, node) + +# Iterate over only live nodes. +live_node_indices(g::Topology, type::IRef) = + imap(Abs, ifilter(_nodes_abs_range(g, type)) do i + is_live(g, i) + end) +live_node_labels(g::Topology, type::IRef) = + imap(live_node_indices(g, type)) do i + node_label(g, i) + end # ========================================================================================== # Edges. -n_edges(top::Topology, type) = top.n_edges[edge_type_index(top, type)] +n_edges(g::Topology, type) = g.n_edges[edge_type_index(g, type)] # Direct neighbourhood when querying particular edge type. # (assuming focal node is not a tombstone) -function _outgoing_indices(top::Topology, node::AbsRef, edge_type::IRef) - i_type = edge_type_index(top, edge_type) - _outgoing_indices(top, node)[i_type] +function _outgoing_indices(g::Topology, node::AbsRef, edge_type::IRef) + i_type = edge_type_index(g, edge_type) + _outgoing_indices(g, node)[i_type] end -function _incoming_indices(top::Topology, node::AbsRef, edge_type::IRef) - i_type = edge_type_index(top, edge_type) - _incoming_indices(top, node)[i_type] +function _incoming_indices(g::Topology, node::AbsRef, edge_type::IRef) + i_type = edge_type_index(g, edge_type) + _incoming_indices(g, node)[i_type] end -outgoing_indices(top::Topology, node::AbsRef, type::IRef) = - imap(Abs, _outgoing_indices(top, node, type)) -incoming_indices(top::Topology, node::AbsRef, type::IRef) = - imap(Abs, _incoming_indices(top, node, type)) -outgoing_labels(top::Topology, node::AbsRef, type::IRef) = - imap(i -> top.nodes_labels[i], _outgoing_indices(top, node, type)) -incoming_labels(top::Topology, node, type::IRef) = - imap(i -> top.nodes_labels[i], _incoming_indices(top, node, type)) +outgoing_indices(g::Topology, node::AbsRef, type::IRef) = + imap(Abs, _outgoing_indices(g, node, type)) +incoming_indices(g::Topology, node::AbsRef, type::IRef) = + imap(Abs, _incoming_indices(g, node, type)) +outgoing_labels(g::Topology, node::AbsRef, type::IRef) = + imap(i -> g.nodes_labels[i], _outgoing_indices(g, node, type)) +incoming_labels(g::Topology, node, type::IRef) = + imap(i -> g.nodes_labels[i], _incoming_indices(g, node, type)) # Direct neighbourhood: return twolevel slices: # first a slice over edge types, then nested neighbours with this edge type. # (assuming focal node is not a tombstone) -function _outgoing_indices(top::Topology, node::AbsRef) - i_node = node_abs_index(top, node) - top.outgoing[i_node.i] +function _outgoing_indices(g::Topology, node::AbsRef) + i_node = node_abs_index(g, node) + g.outgoing[i_node.i] end -function _incoming_indices(top::Topology, node::AbsRef) - i_node = node_abs_index(top, node) - top.incoming[i_node.i] +function _incoming_indices(g::Topology, node::AbsRef) + i_node = node_abs_index(g, node) + g.incoming[i_node.i] end -outgoing_indices(top::Topology, node::AbsRef) = - imap(enumerate(_outgoing_indices(top, node))) do (i_edge_type, _neighbours) +outgoing_indices(g::Topology, node::AbsRef) = + imap(enumerate(_outgoing_indices(g, node))) do (i_edge_type, _neighbours) (i_edge_type, imap(Abs, _neighbours)) end -incoming_indices(top::Topology, node::AbsRef) = - imap(enumerate(_incoming_indices(top, node))) do (i_edge_type, _neighbours) +incoming_indices(g::Topology, node::AbsRef) = + imap(enumerate(_incoming_indices(g, node))) do (i_edge_type, _neighbours) (i_edge_type, imap(Abs, _neighbours)) end -outgoing_labels(top::Topology, node::AbsRef) = - imap(enumerate(_outgoing_indices(top, node))) do (i_edge, _neighbours) - ( - top.edge_types_labels[i_edge], - imap(i_node -> top.nodes_labels[i_node], _neighbours), - ) +outgoing_labels(g::Topology, node::AbsRef) = + imap(enumerate(_outgoing_indices(g, node))) do (i_edge, _neighbours) + (g.edge_types_labels[i_edge], imap(i_node -> g.nodes_labels[i_node], _neighbours)) end -incoming_labels(top::Topology, node::AbsRef) = - imap(enumerate(_incoming_indices(top, node))) do (i_edge, _neighbours) - ( - top.edge_types_labels[i_edge], - imap(i_node -> top.nodes_labels[i_node], _neighbours), - ) +incoming_labels(g::Topology, node::AbsRef) = + imap(enumerate(_incoming_indices(g, node))) do (i_edge, _neighbours) + (g.edge_types_labels[i_edge], imap(i_node -> g.nodes_labels[i_node], _neighbours)) end # Filter adjacency iterators given one particular edge type. # Also return twolevel iterators: focal node, then its neighbours. -function _outgoing_edges_indices(top::Topology, edge_type::IRef) - i_type = edge_type_index(top, edge_type) - imap(ifilter(enumerate(top.outgoing)) do (_, node) +function _outgoing_edges_indices(g::Topology, edge_type::IRef) + i_type = edge_type_index(g, edge_type) + imap(ifilter(enumerate(g.outgoing)) do (_, node) !(node isa Tombstone) end) do (i, _neighbours) (i, _neighbours[i_type]) end end -function _incoming_edges_indices(top::Topology, edge_type::IRef) - i_type = edge_type_index(top, edge_type) - imap(ifilter(enumerate(top.incoming)) do (_, node) +function _incoming_edges_indices(g::Topology, edge_type::IRef) + i_type = edge_type_index(g, edge_type) + imap(ifilter(enumerate(g.incoming)) do (_, node) !(node isa Tombstone) end) do (i, _neighbours) (i, _neighbours[i_type]) end end -outgoing_edges_indices(top::Topology, edge_type::IRef) = - imap(_outgoing_edges_indices(top, edge_type)) do (i_node, _neighbours) +outgoing_edges_indices(g::Topology, edge_type::IRef) = + imap(_outgoing_edges_indices(g, edge_type)) do (i_node, _neighbours) (Abs(i_node), imap(Abs, _neighbours)) end -incoming_edges_indices(top::Topology, edge_type::IRef) = - imap(_incoming_edges_indices(top, edge_type)) do (i_node, _neighbours) +incoming_edges_indices(g::Topology, edge_type::IRef) = + imap(_incoming_edges_indices(g, edge_type)) do (i_node, _neighbours) (Abs(i_node), imap(Abs, _neighbours)) end -outgoing_edges_labels(top::Topology, edge_type::IRef) = - imap(_outgoing_edges_indices(top, edge_type)) do (i_node, _neighbours) - (node_label(top, i_node), imap(i -> node_label(top, i), _neighbours)) +outgoing_edges_labels(g::Topology, edge_type::IRef) = + imap(_outgoing_edges_indices(g, edge_type)) do (i_node, _neighbours) + (node_label(g, i_node), imap(i -> node_label(g, i), _neighbours)) end -incoming_edges_labels(top::Topology, edge_type::IRef) = - imap(_incoming_edges_indices(top, edge_type)) do (i_node, _neighbours) - (node_label(top, i_node), imap(i -> node_label(top, i), _neighbours)) +incoming_edges_labels(g::Topology, edge_type::IRef) = + imap(_incoming_edges_indices(g, edge_type)) do (i_node, _neighbours) + (node_label(g, i_node), imap(i -> node_label(g, i), _neighbours)) end # Same, but filters for one particular node type. -function outgoing_edges_indices(top::Topology, edge_type::IRef, node_type::IRef) - i_et = edge_type_index(top, edge_type) - range = _nodes_abs_range(top, node_type) - imap(ifilter(zip(range, top.outgoing[range])) do (_, node) +function outgoing_edges_indices(g::Topology, edge_type::IRef, node_type::IRef) + i_et = edge_type_index(g, edge_type) + range = _nodes_abs_range(g, node_type) + imap(ifilter(zip(range, g.outgoing[range])) do (_, node) !(node isa Tombstone) end) do (i_node, _neighbours) (Abs(i_node), imap(Abs, ifilter(in(range), _neighbours[i_et]))) end end -function incoming_edges_indices(top::Topology, edge_type::IRef, node_type::IRef) - i_et = edge_type_index(top, edge_type) - range = _nodes_abs_range(top, node_type) - imap(ifilter(zip(range, top.incoming[range])) do (_, node) +function incoming_edges_indices(g::Topology, edge_type::IRef, node_type::IRef) + i_et = edge_type_index(g, edge_type) + range = _nodes_abs_range(g, node_type) + imap(ifilter(zip(range, g.incoming[range])) do (_, node) !(node isa Tombstone) end) do (i_node, _neighbours) (Abs(i_node), imap(Abs, ifilter(in(range), _neighbours[i_et]))) end end -outgoing_edges_labels(top::Topology, edge_type::IRef, node_type::IRef) = - imap(outgoing_edges_indices(top, edge_type, node_type)) do (i_node, neighbours) - (node_label(top, i_node), imap(i -> node_label(top, i), neighbours)) +outgoing_edges_labels(g::Topology, edge_type::IRef, node_type::IRef) = + imap(outgoing_edges_indices(g, edge_type, node_type)) do (i_node, neighbours) + (node_label(g, i_node), imap(i -> node_label(g, i), neighbours)) end -incoming_edges_labels(top::Topology, edge_type::IRef, node_type::IRef) = - imap(incoming_edges_indices(top, edge_type, node_type)) do (i_node, neighbours) - (node_label(top, i_node), imap(i -> node_label(top, i), neighbours)) +incoming_edges_labels(g::Topology, edge_type::IRef, node_type::IRef) = + imap(incoming_edges_indices(g, edge_type, node_type)) do (i_node, neighbours) + (node_label(g, i_node), imap(i -> node_label(g, i), neighbours)) end -function has_edge(top::Topology, type, source::AbsRef, target::AbsRef) - type = edge_type_index(top, type) - source = node_abs_index(top, source) - target = node_abs_index(top, target) - target.i in top.outgoing[source.i][type] +function has_edge(g::Topology, type, source::AbsRef, target::AbsRef) + type = edge_type_index(g, type) + source = node_abs_index(g, source) + target = node_abs_index(g, target) + target.i in g.outgoing[source.i][type] end end diff --git a/src/methods/basic_topology_queries.jl b/src/methods/basic_topology_queries.jl new file mode 100644 index 000000000..107b0c7c7 --- /dev/null +++ b/src/methods/basic_topology_queries.jl @@ -0,0 +1,142 @@ +# ========================================================================================== +# Counts. + +""" + n_live_species(g::Topology) + +Number of live species within the topology. +""" +function n_live_species(g::Topology) + check_species(g) + U.n_nodes(g, :species) +end +export n_live_species + +""" + n_live_nutrients(g::Topology) + +Number of live nutrients within the topology. +""" +function n_live_nutrients(g::Topology) + check_nutrients(g) + U.n_nodes(g, :nutrients) +end +export n_live_nutrients + +""" + n_live_producers(m::Model, g::Topology) + +Number of live producers within the topology. +""" +function n_live_producers(m::InnerParms, g::Topology) + check_species(g) + sp = U.node_type_index(g, :species) + check_species_numbers(m, g, sp) + count = 0 + for i_prod in m.producers_indices + count += U.is_live(g, (T.Rel(i_prod), sp)) + end + count +end +@method n_live_producers depends(Foodweb) + +""" + n_live_consumers(m::Model, g::Topology) + +Number of live consumers within the topology. +""" +function n_live_consumers(m::InnerParms, g::Topology) + check_species(g) + sp = U.node_type_index(g, :species) + check_species_numbers(m, g, sp) + count = 0 + for i_prod in m.consumers_indices + count += U.is_live(g, (T.Rel(i_prod), sp)) + end + count +end +@method n_live_consumers depends(Foodweb) + +# ========================================================================================== +# Iterators. + +""" + live_species(g::Topology) + +Iterate over relative indices of live species within the topology. +""" +function live_species(g::Topology) + check_species(g) + sp = U.node_type_index(g) + imap(U.nodes_indices) do abs + U.node_rel_index(g, (abs, sp)).i + end +end +export live_species + +""" + live_nutrients(g::Topology) + +Iterate over relative indices of live nutrients within the topology. +""" +function live_nutrients(g::Topology) + check_nutrients(g) + nt = U.node_type_index(g) + imap(U.nodes_indices) do abs + U.node_rel_index(g, (abs, nt)).i + end +end +export live_nutrients + +""" + live_producers(g::Topology) + +Iterate over relative indices of live producer species within the topology. +""" +function live_producers(m::InnerParms, g::Topology) + check_species(g) + sp = U.node_type_index(g, :species) + check_species_numbers(m, g, sp) + abs(i_rel) = U.node_abs_index(g, T.Rel(i_rel), sp) + imap(filter(imap(abs, m.producers_indices)) do i_prod + U.is_live(g, i_prod) + end) do i_prod + U.node_rel_index(g, (i_prod, sp)).i + end +end +@method live_producers depends(Foodweb) +export live_producers + +""" + live_consumers(g::Topology) + +Iterate over relative indices of live producer species within the topology. +""" +function live_consumers(m::InnerParms, g::Topology) + check_species(g) + sp = U.node_type_index(g, :species) + check_species_numbers(m, g, sp) + abs(i_rel) = U.node_abs_index(g, T.Rel(i_rel), sp) + imap(filter(imap(abs, m.consumers_indices)) do i_prod + U.is_live(g, i_prod) + end) do i_prod + U.node_rel_index(g, (i_prod, sp)).i + end +end +@method live_consumers depends(Foodweb) +export live_consumers + +""" + trophic_adjacency(g::Topology) + +Produce a two-level iterators yielding predators on first level +and all its preys on the second level. +This only includes :species nodes (and not *eg.* :nutrients). +""" +function trophic_adjacency(g::Topology) + check_species(g) + check_trophic(g) + U.outgoing_edges_labels(g, :trophic, :species) +end +export trophic_adjacency + diff --git a/src/methods/simulate.jl b/src/methods/simulate.jl index 673a0598e..50a1dd1ea 100644 --- a/src/methods/simulate.jl +++ b/src/methods/simulate.jl @@ -17,6 +17,11 @@ function _simulate(model::InnerParms, u0, tmax::Integer; kwargs...) # No default simulation time anymore. given(:tmax) && argerr("Received two values for 'tmax': $tmax and $(take!(:tmax)).") + # If set, produce an @info message + # to warn user about possible degenerated network topologies. + deg_top_arg = :display_degenerated_biomass_graph_properties + deg_top = take_or!(deg_top_arg, true) + # Lower threshold. extinction_threshold = take_or!(:extinction_threshold, 1e-12, Any) extinction_threshold = @tographdata extinction_threshold {Scalar, Vector}{Float64} @@ -28,7 +33,35 @@ function _simulate(model::InnerParms, u0, tmax::Integer; kwargs...) extc = extinction_callback(model, extinction_threshold; verbose) callback = take_or!(:callbacks, Internals.CallbackSet(extc)) - Internals.simulate(model, u0; tmax, extinction_threshold, callback, verbose, kwargs...) + out = Internals.simulate( + model, + u0; + tmax, + extinction_threshold, + callback, + verbose, + kwargs..., + ) + + if deg_top + # Analyze eventual topology. + g = model.topology + biomass = out[end] + restrict_to_live_species!(g, biomass) + diagnostics = [] + for comp in disconnected_components(g) + sp = collect(live_species(g)) + prods = collect(live_producers(model, g)) + cons = collect(live_consumers(model, g)) + ip = isolated_producers(model, comp) + sc = starving_consumers(model, comp) + push!(diagnostics, (sp, prods, cons, ip, sc)) + end + # HERE: display the message suggested in + # https://github.com/BecksLab/EcologicalNetworksDynamics.jl/issues/151#issuecomment-2058641548 + end + + out end @method _simulate depends(FunctionalResponse, ProducerGrowth, Metabolism, Mortality) diff --git a/src/methods/topology.jl b/src/methods/topology.jl index 5802c3cb5..f99afb886 100644 --- a/src/methods/topology.jl +++ b/src/methods/topology.jl @@ -17,31 +17,7 @@ const ifilter = Iterators.filter # Re-export from Topologies. export disconnected_components -# TEMP DEBUG -struct TrophicGraph end - -# Most methods hereafter are only defined -# if :species and/or :trophic compartments do exist. -check_species(g::Topology) = - is_node_type(g, :species) || - argerr("The given topology has no :species node compartment.") -check_trophic(g::Topology) = - is_edge_type(g, :trophic) || - argerr("The given topology has no :trophic edges compartment.") - -""" - trophic_adjacency(g::Topology) - -Produce a two-level iterators yielding predators on first level -and all its preys on the second level. -This only includes :species nodes (and not *eg.* :nutrients). -""" -function trophic_adjacency(g::Topology) - check_species(g) - check_trophic(g) - U.outgoing_edges_labels(g, :trophic, :species) -end -export trophic_adjacency +include("./basic_topology_queries.jl") """ remove_species!(g::Topology, node::Symbol) @@ -151,3 +127,25 @@ function starving_consumers(m::InnerParms, g::Topology) end @method starving_consumers depends(Foodweb) export starving_consumers + +# ========================================================================================== +# Common checks. +check_node_compartment(g::Topology, lab::Symbol) = + is_node_type(g, lab) || + argerr("The given topology has no $(repr(lab)) node compartment.") +check_edge_compartment(g::Topology, lab::Symbol) = + is_edge_type(g, lab) || + argerr("The given topology has no $(repr(lab)) edge compartment.") + +check_species(g::Topology) = check_node_compartment(g, :species) +check_nutrients(g::Topology) = check_node_compartment(g, :nutrients) +check_trophic(g::Topology) = check_edge_compartment(g, :trophic) + +function check_species_numbers(m::InnerParms, g::Topology, i_sp) + a = m.n_species + b = U.n_nodes_including_removed(g, i_sp) + a == b || argerr("Mismatch between the number of species nodes \ + in the given model ($a) and the given topology ($b).") +end +check_species_numbers(m::InnerParms, g::Topology) = # (save this search when you can) + check_species_numbers(m, g, U.edge_type_index(:species)) diff --git a/test/runtests.jl b/test/runtests.jl index c70a4fa77..5b1b41629 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -11,16 +11,16 @@ include("./dedicate_test_failures.jl") # The whole testing suite has been moved to "internals" # while we are focusing on constructing the library API. sep("Test internals.") -include("./internals/runtests.jl") +# include("./internals/runtests.jl") sep("Test System/Blueprints/Components framework.") -include("./framework/runtests.jl") +# include("./framework/runtests.jl") sep("Test API utils.") include("./topologies.jl") -include("./aliasing_dicts.jl") -include("./multiplex_api.jl") -include("./graph_data_inputs/runtests.jl") +# include("./aliasing_dicts.jl") +# include("./multiplex_api.jl") +# include("./graph_data_inputs/runtests.jl") sep("Test user-facing behaviour.") include("./user/runtests.jl") diff --git a/test/user/06-post-simulation.jl b/test/user/06-model_topology.jl similarity index 78% rename from test/user/06-post-simulation.jl rename to test/user/06-model_topology.jl index ec90645e0..a630337b6 100644 --- a/test/user/06-post-simulation.jl +++ b/test/user/06-model_topology.jl @@ -1,37 +1,7 @@ -# Check post-simulation utils. - -using Random -Random.seed!(12) - -#------------------------------------------------------------------------------------------- -@testset "Retrieve model from simulation result." begin - - m = default_model(Foodweb([:a => :b, :b => :c])) - sol = simulate(m, 0.5, 500) - - # Retrieve model from the solution obtained. - msol = get_model(sol) - @test msol == m - - # The value we get is a fresh copy of the state at simulation time. - @test msol !== m # *Not* an alias. - - # Cannot be corrupted afterwards from the original value. - @test m.K[:c] == 1 - m.K[:c] = 2 - @test m.K[:c] == 2 # Okay to keep working on original value. - @test msol.K[:c] == 1 # Still true: simulation was done with 1, not 2. - - # Cannot be corrupted afterwards from the retrieved value itself. - msol.K[:c] = 3 - @test msol.K[:c] == 3 # Okay to work on this one: user owns it. - @test get_model(sol).K[:c] == 1 # Still true. - -end +# Check topology-related utils. @testset "Analyze biomass foodweb topology after species removals." begin - # Simulation not exactly needed for these tests. m = Model(Foodweb([:a => [:b, :c], :b => :d, :c => :d, :e => [:c, :f], :g => :h])) g = m.topology diff --git a/test/user/07-post-simulation.jl b/test/user/07-post-simulation.jl new file mode 100644 index 000000000..e4afd5d3a --- /dev/null +++ b/test/user/07-post-simulation.jl @@ -0,0 +1,30 @@ +# Check post-simulation utils. + +using Random +Random.seed!(12) + +@testset "Retrieve model from simulation result." begin + + m = default_model(Foodweb([:a => :b, :b => :c])) + sol = simulate(m, 0.5, 500) + + # Retrieve model from the solution obtained. + msol = get_model(sol) + @test msol == m + + # The value we get is a fresh copy of the state at simulation time. + @test msol !== m # *Not* an alias. + + # Cannot be corrupted afterwards from the original value. + @test m.K[:c] == 1 + m.K[:c] = 2 + @test m.K[:c] == 2 # Okay to keep working on original value. + @test msol.K[:c] == 1 # Still true: simulation was done with 1, not 2. + + # Cannot be corrupted afterwards from the retrieved value itself. + msol.K[:c] = 3 + @test msol.K[:c] == 3 # Okay to work on this one: user owns it. + @test get_model(sol).K[:c] == 1 # Still true. + +end + diff --git a/test/user/runtests.jl b/test/user/runtests.jl index b7df8d98e..b48f98763 100644 --- a/test/user/runtests.jl +++ b/test/user/runtests.jl @@ -9,7 +9,6 @@ import ..Main: @sysfails, @argfails # Run all .jl files we can find except the current one (and without recursing). only = [] # Unless some files are specified here, in which case only run these. -only = ["./06-post-simulation.jl"] # Unless some files are specified here, in which case only run these. if isempty(only) folder = dirname(@__FILE__) for file in readdir(folder)