diff --git a/.github/workflows/test-mango.yml b/.github/workflows/test-mango.yml index 34919006..37a658e7 100644 --- a/.github/workflows/test-mango.yml +++ b/.github/workflows/test-mango.yml @@ -1,6 +1,12 @@ name: Test Mango.jl -on: [push] +on: + push: + branches: + - main + - development + pull_request: + types: [opened, synchronize, reopened] # needed to allow julia-actions/cache to delete caches diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 00000000..e97a2d32 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,32 @@ +cff-version: "1.2.0" +authors: +- family-names: Sager + given-names: Jens + orcid: "https://orcid.org/0000-0001-6352-4213" +- family-names: Schrage + given-names: Rico + orcid: "https://orcid.org/0000-0001-5339-6553" +doi: 10.5281/zenodo.13860452 +message: If you use this software, please cite our article in the + Journal of Open Source Software. +preferred-citation: + authors: + - family-names: Sager + given-names: Jens + orcid: "https://orcid.org/0000-0001-6352-4213" + - family-names: Schrage + given-names: Rico + orcid: "https://orcid.org/0000-0001-5339-6553" + date-published: 2024-10-01 + doi: 10.21105/joss.07098 + issn: 2475-9066 + issue: 102 + journal: Journal of Open Source Software + publisher: + name: Open Journals + start: 7098 + title: "Mango.jl: A Julia-Based Multi-Agent Simulation Framework" + type: article + url: "https://joss.theoj.org/papers/10.21105/joss.07098" + volume: 9 +title: "Mango.jl: A Julia-Based Multi-Agent Simulation Framework" diff --git a/Project.toml b/Project.toml index 2a3a161a..768f930b 100644 --- a/Project.toml +++ b/Project.toml @@ -2,13 +2,16 @@ name = "Mango" uuid = "5e49fdec-d473-4d14-b295-7bff2fcf1925" authors = ["OFFIS e.V."] repo = "https://github.com/OFFIS-DAI/Mango.jl" -version = "0.4.0" +version = "0.5.0" [deps] +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" ConcurrentCollections = "5060bff5-0b44-40c5-b522-fcd3ca5cecdd" ConcurrentUtilities = "f0e56b4a-5159-44fe-b623-3e5288b988bb" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LightBSON = "a4a7f996-b3a6-4de6-b9db-2fa5f350df41" @@ -17,24 +20,40 @@ MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" Mosquitto = "db317de6-444b-4dfa-9d0e-fbf3d8dd78ea" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +[weakdeps] +GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" + +[extensions] +MangoGraphVisualization = ["Makie", "GraphMakie"] +MangoPlotVisualization = ["Makie"] + [compat] +CairoMakie = "0.13.1" +Colors = "~0.12" ConcurrentCollections = "~0.1" ConcurrentUtilities = "~2.2" +Distributions = "~0.25" +GraphMakie = "0.5.13" +Graphs = "~1.10" JSON = "~0.21" LightBSON = "~0.2" +Makie = "0.22.1" +MetaGraphsNext = "~0.7" Mosquitto = "~0.10" OrderedCollections = "~1.6" Parameters = "~0.12" -Graphs = "~1.11" -MetaGraphsNext = "~0.7" julia = "^1.9" [extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "Documenter"] +test = ["Test", "Documenter", "CairoMakie", "Makie", "GraphMakie"] diff --git a/README.md b/README.md index 19eada0f..1bbcb8c7 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,20 @@

-![logo](docs/src/Logo_mango_ohne_sub.svg#gh-light-mode-only) -![logo](docs/src/Logo_mango_ohne_sub_white.svg#gh-dark-mode-only) +![logo](docs/src/assets/logo.svg#gh-light-mode-only) +![logo](docs/src/assets/logo-dark.svg#gh-dark-mode-only)

[Docs](https://offis-dai.github.io/Mango.jl/stable) | [GitHub](https://github.com/OFFIS-DAI/Mango.jl) | [mail](mailto:mango@offis.de) - ![lifecycle](https://img.shields.io/badge/lifecycle-maturing-blue.svg) [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/OFFIS-DAI/Mango.jl/blob/development/LICENSE) +[![DOI](https://joss.theoj.org/papers/10.21105/joss.07098/status.svg)](https://doi.org/10.21105/joss.07098) [![Test Mango.jl](https://github.com/OFFIS-DAI/Mango.jl/actions/workflows/test-mango.yml/badge.svg)](https://github.com/OFFIS-DAI/Mango.jl/actions/workflows/test-mango.yml) [![codecov](https://codecov.io/gh/OFFIS-DAI/Mango.jl/graph/badge.svg?token=JRZB5T2T2M)](https://codecov.io/gh/OFFIS-DAI/Mango.jl) - - - Mango.jl allows the user to create simple agents with little effort and in the same time offers options to structure agents with complex behaviour. @@ -67,7 +58,10 @@ To add it to your Julia installation or project you can use the Julia REPL by ca The following simple showcase demonstrates how you can define agents in Mango. Jl, assign them to containers and send messages via a TCP connection. For more information on the specifics and other features (e.g. MQTT, modular agent using roles, simulation, tasks), please have a look at our [Documentation](https://offis-dai.github.io/Mango.jl/stable)! -```julia +
+ With Container Creation + + ```julia using Mango # Create the container instances with TCP protocol @@ -94,7 +88,7 @@ function Mango.handle_message(agent::TCPPingPongAgent, message::Any, meta::Any) agent.counter += 1 println( - "$(agent.aid) got a message: $message." * + "$(aid(agent)) got a message: $message." * "This is message number: $(agent.counter) for me!" ) @@ -120,17 +114,46 @@ activate([container, container2]) do sleep(1) end end -``` + ``` +
+ +In newer versions of Mango.jl, the express API is introduced, which rewrites the code above to: + +
+ With Express API + + ```julia +using Mango + +@agent struct TCPPingPongAgent + counter::Int +end + +function Mango.handle_message(agent::TCPPingPongAgent, message::Any, meta::Any) + agent.counter += 1 + + println( + "$(aid(agent)) got a message: $message." * + "This is message number: $(agent.counter) for me!" + ) + + sleep(0.5) + + if message == "Ping" + reply_to(agent, "Pong", meta) + elseif message == "Pong" + reply_to(agent, "Ping", meta) + end +end + +ping_agent = TCPPingPongAgent(0) +pong_agent = TCPPingPongAgent(0) +run_with_tcp(2, (ping_agent, :aid => "Agent_1"), (pong_agent, :aid => "Agent_2")) do cl + send_message(ping_agent, "Ping", address(pong_agent)) + sleep_until(() -> ping_agent.counter >= 5) +end + ``` +
## License Mango.jl is developed and published under the MIT license. - - - - - - - diff --git a/docs/Project.toml b/docs/Project.toml index 5b222d7f..14febf2f 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,9 +1,9 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Mango = "5e49fdec-d473-4d14-b295-7bff2fcf1925" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" -Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" [compat] Documenter = "~0.27" diff --git a/docs/make.jl b/docs/make.jl index 5cc63181..ecdc7ec0 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -6,9 +6,9 @@ logger = Test.TestLogger(min_level=Info); with_logger(logger) do makedocs( modules=[Mango], - format=Documenter.HTML(; prettyurls=get(ENV, "CI", nothing) == "true"), + format=Documenter.HTML(; assets=["assets/mango_theme_overrides.css"], prettyurls=get(ENV, "CI", nothing) == "true"), authors="mango Team", - sitename="Mango.jl", + sitename="Mango.jl Documentation", pages=Any["Home"=>"index.md", "Getting Started"=>"getting_started.md", "Agents"=>"agent.md", diff --git a/docs/src/api.md b/docs/src/api.md index 79d109c2..62f67b22 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -17,7 +17,7 @@ Here, the API for the agent structs created with @agent/@role is listed. ```@autodocs Modules = [Mango] Private = false -Pages = ["agent/api.jl", "agent/core.jl", "agent/role.jl"] +Pages = ["agent/api.jl", "agent/core.jl", "agent/role.jl", "agent/services.jl"] Order = [:macro, :function, :constant, :type, :module] ``` @@ -33,12 +33,22 @@ Pages = ["container/api.jl", "container/core.jl", "container/mqtt.jl", "containe # Simulation -In the following the APIs regarding the simulation container are listed. +In the following the APIs regarding the simulation world are listed. ```@autodocs Modules = [Mango] Private = false -Pages = ["container/simulation.jl", "simulation/communication.jl", "simulation/tasks.jl"] +Pages = ["simulation/container.jl", "simulation/communication.jl", "simulation/tasks.jl", "simulation/world.jl"] +``` + +# Simulation Environment + +In the following the APIs regarding the simulation environment are listed. + +```@autodocs +Modules = [Mango] +Private = false +Pages = ["environment/api.jl", "environment/core.jl"] ``` # Scheduling @@ -58,7 +68,7 @@ In the following the APIs for creating, aplying and using topologies is listed. ```@autodocs Modules = [Mango] Private = false -Pages = ["world/topology.jl"] +Pages = ["util/topology.jl"] ``` # Encoding/Decoding diff --git a/docs/src/Logo_mango_ohne_sub_white.svg b/docs/src/assets/logo-dark.svg similarity index 100% rename from docs/src/Logo_mango_ohne_sub_white.svg rename to docs/src/assets/logo-dark.svg diff --git a/docs/src/Logo_mango_ohne_sub.svg b/docs/src/assets/logo.svg similarity index 100% rename from docs/src/Logo_mango_ohne_sub.svg rename to docs/src/assets/logo.svg diff --git a/docs/src/assets/mango_theme_overrides.css b/docs/src/assets/mango_theme_overrides.css new file mode 100644 index 00000000..20ebbcc7 --- /dev/null +++ b/docs/src/assets/mango_theme_overrides.css @@ -0,0 +1,91 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI emojiEmoji; +} + +html.theme--documenter-dark body { + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI emojiEmoji; +} + +html.theme--documenter-dark #documenter .docs-sidebar { + border-right: none; +} + +html.theme--documenter-dark .select select, +html.theme--documenter-dark .textarea, +html.theme--documenter-dark .input, +html.theme--documenter-dark #documenter .docs-sidebar form.docs-search>input { + border-color: #2f4547; +} + +#documenter { + width: 100%; + display: flex; +} + +@media screen and (min-width: 1056px) { + #documenter .docs-sidebar { + position: sticky; + height: 100vh; + padding-left: calc(50% - 1200px / 2); + display: flex; + min-width: 18rem; + box-sizing: content-box; + } + + html.theme--documenter-dark #documenter .docs-sidebar { + position: sticky; + height: 100vh; + padding-left: calc(50% - 1200px / 2); + display: flex; + min-width: 18rem; + box-sizing: content-box; + background-color: #252929; + } +} + +html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu .tocitem { + background-color: transparent; +} + +@media screen and (min-width: 1056px) { + #documenter .docs-main { + max-width: 52rem; + padding-right: 1rem; + margin-left: 50px; + } + + html.theme--documenter-dark #documenter .docs-main { + max-width: 52rem; + padding-right: 1rem; + margin-left: 50px; + } +} + +#documenter .docs-sidebar .docs-package-name { + visibility: hidden; + height: 0; +} + +html.theme--documenter-dark .button.is-static { + border-color: #2f4547; +} + +html.theme--documenter-dark #documenter .docs-main header.docs-navbar { + border-bottom: 1px solid #2f4547; +} + +html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu ul.internal { + border-top: 1px solid #2f4547; +} + +html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu li.is-active { + border-color: #2f4547; +} + +html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu { + border-color: #2f4547; +} + +html.theme--documenter-dark #documenter .docs-main .docs-footer { + border-color: #2f4547; +} \ No newline at end of file diff --git a/docs/src/getting_started.md b/docs/src/getting_started.md index 520ea884..2cb52525 100644 --- a/docs/src/getting_started.md +++ b/docs/src/getting_started.md @@ -4,7 +4,7 @@ In this getting started guide, we will explore the essential features of Mango.j You can also find working examples of the following code in [examples.jl](../../test/examples.jl). -## 0. Quickstart +## Quickstart In Mango.jl, you can define agents using a number of roles using [`@role`](@ref) and [`agent_composed_of`](@ref), or directly using [`@agent`](@ref). To define the behavior of the agents, [`handle_message`](@ref) can be defined, and messages can be send using [`send_message`](@ref). To run the agents with a specific protocol in real time the fastest way is to use [`run_with_tcp`](@ref), which will distribute the agents to tcp-containers and accepts a function in which some agent intializiation and/or trigger-code could be put. The following example illustrates the basic usage of the functions. @@ -70,7 +70,7 @@ function Mango.handle_message(agent::TCPPingPongAgent, message::Any, meta::Any) agent.counter += 1 println( - "$(agent.aid) got a message: $message." * + "$(aid(agent)) got a message: $message." * "This is message number: $(agent.counter) for me!" ) diff --git a/docs/src/scheduling.md b/docs/src/scheduling.md index 9ec9408b..c46ec1ac 100644 --- a/docs/src/scheduling.md +++ b/docs/src/scheduling.md @@ -8,6 +8,7 @@ The module provides different [`TaskData`](@ref) types, each catering to specifi 1. [`PeriodicTaskData`](@ref): For tasks that need to be executed periodically, it holds the time interval in seconds between task executions. 2. [`InstantTaskData`](@ref): For tasks that need to be executed instantly, without any delay. +2. [`DelayTaskData`](@ref): For tasks that need to be executed with a specific delay once. 3. [`DateTimeTaskData`](@ref): For tasks that need to be executed at a specific date and time. 4. [`AwaitableTaskData`](@ref): For tasks that require waiting for an awaitable object to complete before execution. 5. [`ConditionalTaskData`](@ref): For tasks that execute based on a specific condition at regular intervals. @@ -85,6 +86,7 @@ The [`execute_task`](@ref) function executes a task with a specific [`TaskData`] ```julia execute_task(f::Function, data::PeriodicTaskData) execute_task(f::Function, data::InstantTaskData) +execute_task(f::Function, data::DelayTaskData) execute_task(f::Function, data::DateTimeTaskData) execute_task(f::Function, data::AwaitableTaskData) execute_task(f::Function, data::ConditionalTaskData) diff --git a/docs/src/simulation.md b/docs/src/simulation.md index a62c2af9..8706761b 100644 --- a/docs/src/simulation.md +++ b/docs/src/simulation.md @@ -4,7 +4,10 @@ The simulation container has the same role as the real-time container and theref ## Create and stepping a simulation container -To create a simulation container, it is advised to use `create_simulation_container`. This method will create a clock with the given simulation time and set default for the communication simulation and the general task simulation. In most cases the default task simulation will be what you desire. The communication simulation object (based on the abstract type `CommunicationSimulation`) is used to determine the delays of the messages in the simulation, while the task simulation determines the way the tasks are scheduled (within a time step, using parallelization etc.) in the simulation. +To create a simulation container, it is advised to use `create_world`. +This method will create a clock with the given simulation time and set default for the communication simulation and the general task simulation. +In most cases the default task simulation will be what you desire. +The communication simulation object (based on the abstract type `CommunicationSimulation`) is used to determine the delays of the messages in the simulation, while the task simulation determines the way the tasks are scheduled (within a time step, using parallelization etc.) in the simulation. In the following example a simple simulation is executed. @@ -18,14 +21,14 @@ end # Create a communication simulator, the simple communication simulator works with static delays between specific agents and a global default, here 0 comm_sim = SimpleCommunicationSimulation(default_delay_s=0) # Set the simulation time to an initial value -container = create_simulation_container(DateTime(Millisecond(10)), communication_sim=comm_sim) +container = create_world(DateTime(Millisecond(10)), communication_sim=comm_sim) # Creating agents and registering, no difference here to the real time container agent1 = register(container, SimAgent()) agent2 = register(container, SimAgent()) # Send a message from agent2 to agent1, the message will be written to a queue instead of processed by some protocol -send_message(agent2, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) +send_message(agent2, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1))) # in this stepping call the message will be delivered and handled to/by the agent1 # step_size=1, if no size is specified the simulation will work as discrete event simulation, executing all tasks occurring on the next event time. diff --git a/docs/src/topology.md b/docs/src/topology.md index 7c1f86c1..fa052f8d 100644 --- a/docs/src/topology.md +++ b/docs/src/topology.md @@ -6,7 +6,13 @@ As it can be pretty clunky to create every neighborhood-list manually, Mango.jl # Creating topologies -First, there are several pre-defined topologies. It is also possible to use an arbitrary Graphs.jl graph. After the creation of the topology, the agents need to be added to the topology. This can be done with `per_node(topology) do node ... end`. In the do-block it is possible to add agents to nodes, the do-block will be executed per vertex of your graph. +There are two ways to create a working topology: +1. You choose a graph and an assignment mechanism. +2. You create the whole topology and do the assignment of the agents to each node manualy. + +## Graph + Assignment Mechanism + +To start creating a topology you can use several pre-defined topologies. It is also possible to use an arbitrary Graphs.jl graph. ```@example using Mango, Graphs @@ -18,15 +24,15 @@ topology = cycle_topology(3) # cycle topology = complete_topology(3) # fully connected topology = graph_topology(complete_digraph(3)) # based on arbitrary Graphs.jl AbstractGraph -per_node(topology) do node - add!(node, MyAgent()) -end - # resulting topology graph topology.graph ``` -However, often this approach is not feasible, because you create a specific agent system with agents which need to be linked in a very specific way, such that it is not possible to assign the same agent type to every node. For this reason you can define the topology manually: +After the topology is instantiated the agents need to be assigned in way that suits your goal of communication structure between the agents. Mango.jl provides some eays-to-use functions for that: [`per_node`](@ref), [`auto_assign!`](@ref), [`assign_agents!`](@ref), [`choose_agents!`](@ref). + +## Manual Creation + +However, sometimes it is easier to manually define everything, because you create a specific agent system with agents which need to be linked in a very specific way. For this reason you can define the topology manually: ```@example using Mango @@ -49,6 +55,38 @@ end topology_neighbors(container[1]) ``` +If you need to modify a topology manually you can use [`modify_topology`](@ref). + + +# Inspecting topologies + +Functions that are defined on `Graphs.jl`graphs have been extended with methods for topologies so the following calls will resolve normally. +Note that this requires `using Graphs` as well as `using Mango` to resolve correctly: +```julia +using Graphs, Mango +topology = complete_topology(5) + +edges(topology) # SimpleEdgeIter 10 +edgetype(topology) # Graphs.SimpleGraphs.SimpleEdge{Int64} +has_edge(topology, 1, 2) # true +has_vertex(topology, 1) # true +inneighbors(topology, 2) # [1, 3, 4, 5] +outneighbors(topology, 2) # [1, 3, 4, 5] +is_directed(topology) # false +ne(topology) # 10 +nv(topology) # 5 +vertices(topology) # [1, 2, 3, 4, 5] +``` + # Using the topology -At this point we know how to create topologies and how to populate them. To actually use them, the function [`topology_neighbors`](@ref) exists. The function returns a vector of AgentAddress objects, which represent all other agents in the neighborhood of `agent`. +At this point, we know how to create topologies and how to populate them. To actually use them, the function [`topology_neighbors`](@ref) exists. The function returns a vector of AgentAddress objects, which represent all other agents in the neighborhood of `agent`. + +# Connecting topologies together + +Sometimes, systems become so complex that creating multiple simple topologies is easier than creating a single complex topology. If you use more than one topology, you can connect your topologies using so-called `connectors`. + +`Connectors` are single agents that act as connection points between topologies. A connector can accept specific `connection types`. A connection type is a `Symbol` (e.g. :default), which specifies the type of connection a connector can establish. To mark an agent as a connector, you can use [`mark_as_connector!`](@ref). + +If you connect two topologies, such as topology A and topology B, using a specific connection type c, all connectors of A and B will be linked if they are connectors for the same connection type c. Imagine there is one connector in A and one in B that are defined for the same connection type. This would result in an extended neighborhood for the connector in A, which now includes the connector from B and vice versa. To access the extended neighborhood, you can use the `include + diff --git a/ext/MangoGraphVisualization/MangoGraphVisualization.jl b/ext/MangoGraphVisualization/MangoGraphVisualization.jl new file mode 100644 index 00000000..a28420fe --- /dev/null +++ b/ext/MangoGraphVisualization/MangoGraphVisualization.jl @@ -0,0 +1,6 @@ + +module MangoGraphVisualization + +include("src/communication.jl") + +end \ No newline at end of file diff --git a/ext/MangoGraphVisualization/src/communication.jl b/ext/MangoGraphVisualization/src/communication.jl new file mode 100644 index 00000000..fcd00ce0 --- /dev/null +++ b/ext/MangoGraphVisualization/src/communication.jl @@ -0,0 +1,369 @@ + +using Mango +using Makie +using GraphMakie.NetworkLayout +using GraphMakie +using Graphs +using Dates +using MetaGraphsNext + +function Mango.plot_node_topology(topology::Topology; write_to::Union{Nothing,String}="topology.svg", ax=nothing, fig=Figure()) + + if isnothing(ax) + ax = Axis(fig[1, 1]) + end + + g = topology.graph + graphplot!(ax, g, layout=Shell(), + elabels=["$i" for i in 1:ne(g)], + arrow_show=true, + node_size=48, + node_color=:gray, + arrow_size=24, + ilabels=repr.(1:nv(g)), + ilabels_color=:white) + + hidedecorations!(ax) + hidespines!(ax) + + if !isnothing(write_to) + save(write_to, fig) + end +end + +function combine_simple_graphs(graphs::Vector{<:MetaGraph}) + # Count total vertices + total_vertices = sum(nv(g) for g in graphs) + combined = SimpleGraph(total_vertices) + + offset = 0 + for g in graphs + for e in edges(g) + s = src(e) + offset + d = dst(e) + offset + add_edge!(combined, s, d) + end + offset += nv(g) + end + return combined +end + +function combine_meta_graphs(graphs::Vector{<:MetaGraph}) + vertices_description::Vector{Pair{String,Agent}} = [] + edges_description::Vector{Pair{Tuple{String,String},State}} = [] + offset = 0 + for graph in graphs + vertices_description = [vertices_description; ["$(label_for(graph, i))-$offset" => graph[label_for(graph, i)] for i in vertices(graph)]] + edges_description = [edges_description; [("$(label_for(graph, src(e)))-$offset", "$(label_for(graph, dst(e)))-$offset") => NORMAL for e in edges(graph)]] + offset += 1 + end + + return MetaGraph(combine_simple_graphs(graphs), vertices_description, edges_description) +end + +function Mango.plot_multi_agent_topology(topologies::Vector{Topology}; write_to::Union{Nothing,String}="multi_topology.svg") + graphs = [topology_to_aid_graph(top) for top in topologies] + g = combine_meta_graphs(graphs) + for top in topologies + for (connected_type, connected_topology) in top.connections + for (type, connector) in top.connectors + if type == connected_type + for (other_type, other_connector) in connected_topology.connectors + if other_type == type + for i in 0:(length(topologies)-1) + offset_i = i + for j in 0:(length(topologies)-1) + offset_j = j + aid_f = "$(connector.address.aid)-$offset_i" + aid_s = "$(other_connector.address.aid)-$offset_j" + if aid_f != aid_s + g[aid_f, aid_s] = EXT_CONNECTION + end + end + end + end + end + end + end + end + end + + fig=Figure() + ax = Axis(fig[1, 1]) + + graphplot!(ax, g, layout=Stress(), + elabels=["" for e in edges(g)], + node_size=18, + node_color=:gray, + ilabels=[name(g[label_for(g,i)]) for i in 1:nv(g)], + ilabels_color=:white, + ilabels_fontsize=5) + + hidedecorations!(ax) + hidespines!(ax) + + if !isnothing(write_to) + save(write_to, fig) + end +end + +function _to_seconds(date::DateTime, init::DateTime) + return (date - init).value / 1000 +end + +function _find_edge(graph::AbstractGraph, sender_id::Int, receiver_id::Int) + for (i, edge) in enumerate(edges(graph)) + if collect(labels(graph))[src(edge)] == sender_id && collect(labels(graph))[dst(edge)] == receiver_id + return i, edge + end + end + return -1 +end + +@agent struct VisuProxyAgent + proxy_aid::String +end + +function Mango.aid(agent::VisuProxyAgent) + return agent.proxy_aid +end + +function _create_node_in_for_maybe(g, aid_to_node_id, aid) + if !haskey(aid_to_node_id, aid) + next_id = length(labels(g)) == 0 ? 1 : maximum(collect(labels(g))) + 1 + + g[next_id] = Node(id=next_id, agents=[VisuProxyAgent(aid)]) + aid_to_node_id[aid] = next_id + return next_id + end + return aid_to_node_id[aid] +end + +function _create_aid_based_data(g, nid, aid_to_x, default) + label = label_for(g, nid) + agents = g[label].agents + if length(agents) > 0 + c_aid = aid(agents[1]) + return get(aid_to_x, c_aid, default) + end + return "$label" +end + +function Mango.show_communication_data(messages::Vector{MessageTransaction}, + initial_time::DateTime=DateTime(0); + resolution_s::Real=0.1, + show::Bool=true, + size=(1200, 800), + based_on::Union{Nothing,MetaGraph,Topology}=nothing, + layout=Spring(C=4), + aid_to_name::Dict{String,String}=nothing, + aid_to_color::Dict{String,Symbol}=nothing) + + g = based_on + if based_on isa Topology + g = g.graph + end + + if isnothing(based_on) + g = MetaGraph( + DiGraph(); + label_type=Int, + vertex_data_type=Node, + edge_data_type=State, + ) + else + if !is_directed(g) + edge_data = [[(e[1], e[2]) => g[e[1], e[2]] for e in edge_labels(g)]; + [(e[2], e[1]) => g[e[1], e[2]] for e in edge_labels(g)]] + vertex_data = [l => g[l] for l in labels(g)] + underlying_graph = DiGraph(g.graph) + g = MetaGraph(underlying_graph, vertex_data, edge_data) + end + end + + aid_to_node_id = Dict{String,Int}() + for label in labels(g) + node = g[label] + for agent in node.agents + aid_to_node_id[aid(agent)] = label + end + end + + for message in messages + old_len = length(g) + first_node_label = _create_node_in_for_maybe(g, aid_to_node_id, message.sender_id) + second_node_label = _create_node_in_for_maybe(g, aid_to_node_id, message.receiver_id) + + if old_len != length(g) + # edge did not exist before + g[first_node_label, second_node_label] = UNKNOWN + else + # edge may exist + first_node = code_for(g, first_node_label) + second_node = code_for(g, second_node_label) + if !has_edge(g, first_node, second_node) + g[first_node_label, second_node_label] = UNKNOWN + end + end + end + + fig = Figure(size=size) + ax = Axis(fig[1, 1]) + + min_date = initial_time + max_date = max([m.arriving_date for m in messages]...) + + delta = _to_seconds(max_date, min_date) + resolution_s + sg = SliderGrid(fig[2, 1], + (label="Time", range=0:resolution_s:delta, format="{:.2f}", startvalue=0), + tellheight=true) + + sliderobservable = sg.sliders[1].value + + edgecolors = lift(sliderobservable) do time + edgecolors = [:black for i in 1:ne(g)] + for message in messages + if time >= _to_seconds(message.sent_date, min_date) && + time < _to_seconds(message.arriving_date, min_date) + + found = _find_edge(g, aid_to_node_id[message.sender_id], + aid_to_node_id[message.receiver_id]) + if found != -1 + edgecolors[found[1]] = :red + end + end + end + edgecolors + end + elabels = lift(sliderobservable) do time + i_elabels = ["" for _ in 1:ne(g)] + for message in messages + if time >= _to_seconds(message.sent_date, min_date) && + time < _to_seconds(message.arriving_date, min_date) + + found = _find_edge(g, aid_to_node_id[message.sender_id], aid_to_node_id[message.receiver_id]) + if found != -1 + i_elabels[found[1]] = "$(typeof(message.content))" + end + end + end + i_elabels + end + + efull = lift(sliderobservable) do time + i_efull = ["" for _ in 1:ne(g)] + for message in messages + if time >= _to_seconds(message.sent_date, min_date) && + time < _to_seconds(message.arriving_date, min_date) + + found = _find_edge(g, aid_to_node_id[message.sender_id], aid_to_node_id[message.receiver_id]) + if found != -1 + i_efull[found[1]] = "$(message.content)" + end + end + end + i_efull + end + + arrow_markers = lift(sliderobservable) do time + markers = [:hline for _ in 1:ne(g)] + for message in messages + if time >= _to_seconds(message.sent_date, min_date) && + time < _to_seconds(message.arriving_date, min_date) + + found = _find_edge(g, aid_to_node_id[message.sender_id], aid_to_node_id[message.receiver_id]) + if found != -1 + markers[found[1]] = :rtriangle + end + end + end + markers + end + + arrow_shifts = lift(sliderobservable) do time + shifts = [1.0 for _ in 1:ne(g)] + for message in messages + if time >= _to_seconds(message.sent_date, min_date) && + time < _to_seconds(message.arriving_date, min_date) + + found = _find_edge(g, aid_to_node_id[message.sender_id], aid_to_node_id[message.receiver_id]) + if found != -1 + shifts[found[1]] = 0.8 + end + end + end + shifts + end + + edge_width = lift(sliderobservable) do time + ew = [1.0 for _ in 1:ne(g)] + for message in messages + if time >= _to_seconds(message.sent_date, min_date) && + time < _to_seconds(message.arriving_date, min_date) + + found = _find_edge(g, aid_to_node_id[message.sender_id], aid_to_node_id[message.receiver_id]) + if found != -1 + ew[found[1]] = 6 + end + end + end + ew + end + + ilabels = [_create_aid_based_data(g, i, aid_to_name, "unknown") for i in 1:nv(g)] + node_colors = [_create_aid_based_data(g, i, aid_to_color, :gray) for i in 1:nv(g)] + + p = graphplot!(ax, g, layout=layout, + edge_color=edgecolors, + elabels=elabels, + arrow_show=true, + edge_width=edge_width, + node_size=48, + node_color=node_colors, + node_strokewidth=0, + arrow_size=24, + arrow_shift=arrow_shifts, + arrow_marker=arrow_markers, + ilabels=ilabels, + ilabels_color=:white, + elabels_attr=(word_wrap_width=5,)) + + hidedecorations!(ax) + hidespines!(ax) + + deregister_interaction!(ax, :rectanglezoom) + register_interaction!(ax, :ndrag, NodeDrag(p)) + + function edge_hover_action(state, idx, event, axis) + if !state + sliderobservable[] = sliderobservable[] + end + p.elabels[][idx] = state ? efull[][idx] : elabels[][idx] + p.elabels[] = p.elabels[] + end + ehover = EdgeHoverHandler(edge_hover_action) + register_interaction!(ax, :ehover, ehover) + + if show + return display(fig) + else + save("communication.svg", fig) + end + return fig +end + +function Mango.show_communication_data(world::World; + resolution_s::Real=0.1, + show::Bool=true, + based_on::Union{Nothing,MetaGraph,Topology}=nothing) + + aid_to_name = Dict(aid(agent) => name(agent) for agent in agents(world)) + aid_to_color = Dict(aid(agent) => color(agent) for agent in agents(world)) + return show_communication_data(world.recorded_messages, + world.clock.initial_time, + resolution_s=resolution_s, + show=show, + based_on=based_on, + aid_to_name=aid_to_name, + aid_to_color=aid_to_color) +end \ No newline at end of file diff --git a/ext/MangoPlotVisualization/MangoPlotVisualization.jl b/ext/MangoPlotVisualization/MangoPlotVisualization.jl new file mode 100644 index 00000000..37d57e4c --- /dev/null +++ b/ext/MangoPlotVisualization/MangoPlotVisualization.jl @@ -0,0 +1,6 @@ + +module MangoPlotVisualization + +include("src/observation.jl") + +end \ No newline at end of file diff --git a/ext/MangoPlotVisualization/src/observation.jl b/ext/MangoPlotVisualization/src/observation.jl new file mode 100644 index 00000000..bb9bfb84 --- /dev/null +++ b/ext/MangoPlotVisualization/src/observation.jl @@ -0,0 +1,108 @@ + +using Mango +using Makie + +function Mango.plot_world(world::World, recording::String; + write_to::Union{Nothing,String}="world_observation.png", + fig=Figure(), + color=:black, + colormap=:default) + + data = data_collection(world, recording) + ax = Axis(fig, + title="$recording over time", + xlabel="time (seconds)", + ylabel=recording, + ) + lines!(ax, data.time, data.timeseries, color=color, colormap=colormap) + if !isnothing(write_to) + save(write_to, fig) + end +end + +function Mango.plot_agents(world::World, recording::String; + write_to::Union{Nothing,String}="agent_observation.png", + fig=Figure(), + color=:viridis) + + data = data_agent_collection(world, recording) + pairs = collect(data.timeseries) + labels = [pair[1] for pair in pairs] + values = [pair[2] for pair in pairs] + if data.dedicated_plots + for (i, agent) in enumerate(labels) + ax = Axis(fig[i], + title="$recording over time for $agent", + xlabel="time (seconds)", + ylabel=recording, + ) + lines!(ax, data.time, values[i], color=:black) + end + else + ax = Axis(fig[1,1], + title="$recording over time for each agent", + xlabel="time (seconds)", + ylabel=recording, + ) + series!(ax, data.time, hcat(values...)', labels=labels, color=color) + Legend(fig[1, 2], ax) + end + if !isnothing(write_to) + save(write_to, fig) + end +end + +function _create_label(layout, label, y) + return Label(layout[y, 1, TopLeft()], label, + fontsize=26, + font=:bold, + padding=(0, 5, 5, 0), + halign=:left) +end + +function Mango.plot_recordings(world::World; + write_to::Union{Nothing,String}="observation.png", + size=:auto, + color=:black, + colormap=:viridis) + + row_length = 3 + dc = Dict(key => value for (key,value) in world.data_collections if !value.no_plot) + dac = Dict(key => value for (key,value) in world.data_agent_collections if !value.no_plot) + if size == :auto + dac_length = sum([record.dedicated_plots ? length(record.timeseries) : 1 for record in values(dac)]) + size = ( + min(max(length(dc), dac_length), 3) * 400, + 600 + ((length(dc)-1) ÷ 3 + (dac_length-1) ÷ 3) * 250 + ) + end + main_fig = Figure(size=size) + all_layout = main_fig[1, 1] = GridLayout() + + _create_label(all_layout, "W", 1) + for (i, key) in enumerate(keys(dc)) + layout_fig = all_layout[((i - 1) ÷ row_length) + 1, ((i - 1) % row_length) + 1] + plot_world(world, key, write_to=nothing, fig=layout_fig, color=color, colormap=colormap) + end + + y_start_agents = (((length(dc) - 1) ÷ row_length) + 2) + shift = (y_start_agents-1) * row_length + + _create_label(all_layout, "A", y_start_agents) + for (i, key) in enumerate(keys(dac)) + layout_fig = all_layout[(((i+shift) - 1) ÷ row_length) + 1, (((i+shift) - 1) % row_length) + 1] + current_dac = dac[key] + if current_dac.dedicated_plots + layouts = [all_layout[(((i+shift+j-1) - 1) ÷ row_length) + 1, (((i+shift+j-1) - 1) % row_length) + 1] + for j in 1:length(current_dac.timeseries)] + plot_agents(world, key, write_to=nothing, fig=layouts, color=colormap) + shift += length(current_dac.timeseries) - 1 + else + plot_agents(world, key, write_to=nothing, fig=layout_fig, color=colormap) + end + end + + if !isnothing(write_to) + return save(write_to, main_fig) + end +end \ No newline at end of file diff --git a/paper/paper.bib b/paper/paper.bib index 1bd52bd9..57d8fe6b 100644 --- a/paper/paper.bib +++ b/paper/paper.bib @@ -1,5 +1,5 @@ @article{schrage:2024, - title={mango: A modular python-based agent simulation framework}, + title={mango: A modular {P}ython-based agent simulation framework}, author={Schrage, Rico and Sager, Jens and H{\"o}rding, Jan Philipp and Holly, Stefanie}, journal={SoftwareX}, volume={27}, @@ -10,7 +10,7 @@ @article{schrage:2024 } @book{russel:2010, - title={Artificial intelligence a modern approach}, + title={Artificial intelligence: a modern approach}, author={Russell, Stuart J and Norvig, Peter}, year={2010}, publisher={Prentice Hall}, @@ -104,8 +104,6 @@ @article{agents:2022 author = {George Datseris and Ali R. Vahdati and Timothy C. DuBois}, title = {Agents.jl: a performant and feature-full agent-based modeling software of minimal code complexity}, journal = {{SIMULATION}}, - volume = {0}, - number = {0}, } @inproceedings{netlogo:2004, @@ -127,7 +125,7 @@ @inproceedings{mesa:2020 and Dancy, Christopher and Hyder, Ayaz and Hussain, Muhammad", - title="Utilizing Python for Agent-Based Modeling: The Mesa Framework", + title="Utilizing {P}ython for Agent-Based Modeling: The Mesa Framework", booktitle="Social, Cultural, and Behavioral Modeling", year="2020", publisher="Springer International Publishing", @@ -191,4 +189,4 @@ @article{julia:2017 author = {Jeff Bezanson and Alan Edelman and Stefan Karpinski and Viral B. Shah}, title = {Julia: A Fresh Approach to Numerical Computing}, journal = {{SIAM} Review} -} \ No newline at end of file +} diff --git a/paper/paper.md b/paper/paper.md index 533e07f5..96ccd365 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -31,7 +31,7 @@ bibliography: paper.bib # Summary Multi-agent simulations are inherently complex, making them difficult to implement, maintain, and optimize. -An agent, as defined by [@russel:2010], is software that perceives its environment through sensors and acts upon it using actuators. +An agent, as defined by @russel:2010, is software that perceives its environment through sensors and acts upon it using actuators. `Mango.jl` is a simulation framework for multi-agent systems implemented in Julia [@julia:2017]. It enables quick implementations of multiple communicating agents, either spanning multiple devices or in a single local environment. @@ -50,7 +50,7 @@ This is useful for simulations, where simulated time should run much faster than # Statement of need -Applications of multi-agent systems can be found in various fields, such as in distributed optimization [@yang:2019], reinforcement learning [@gronauer:2022], robotics [@chen:2019] and more. +Applications of multi-agent systems can be found in various fields, such as in distributed optimization [@yang:2019], reinforcement learning [@gronauer:2022], robotics [@chen:2019], and more. Many of these systems are highly complex and feature heterogeneous and interacting actors. This makes them inherently difficult to model and develop. Therefore, a structured development framework to support this process is a valuable asset. @@ -58,8 +58,8 @@ Therefore, a structured development framework to support this process is a valua While `Mango.jl` is a general purpose multi-agent framework, we will focus on energy systems in the following as this is the domain the authors are most familiar with. Many of the ideas for `Mango.jl` are based on the existing Python framework `mango` [@schrage:2024]. -The main reason for this julia-based version is to allow better focus on simulation performance, enabling larger scales of multi-agent simulations. -This is especially relevant in the energy domain, where an increasing amount of energy resources (e.g. batteries and PV-generators) have distributed ownership, competing goals and contribute to the same power grid. +The main reason for this Julia-based version is to allow better focus on simulation performance, enabling larger scales of multi-agent simulations. +This is especially relevant in the energy domain, where an increasing amount of energy resources (e.g., batteries and PV-generators) have distributed ownership, competing goals and contribute to the same power grid. Large scale multi-agent simulations allow researchers to study the behavior of these participants in energy markets and grid simulations. The Python version of `mango` has already been successfully applied to various research areas in the energy domain, including coalition formation in multi-energy networks [@schrage:2023], distributed market participation of battery storage units [@tiemann:2022], distributed black start [@stark:2021], and investigating the impact of communication topologies on distributed optimization heuristics [@holly:2021]. @@ -69,8 +69,8 @@ New Julia-based projects using `Mango.jl` are in active development. To our knowledge, there is no Julia-based multi-agent framework with a focus on agent communication and distributed operation like `Mango.jl`. `Agents.jl` [@agents:2022] is a multi-agent framework for modeling agent interactions in a defined space to observe emergent properties like in animal flocking behavior or the spreading of diseases. -This puts it in line with frameworks like mesa [@mesa:2020] or NetLogo [@netlogo:2004]. -These have a different scope than `Mango.jl` which is more focused on agent communication and internal agent logic for software applications. +This puts it in line with frameworks like mesa [@mesa:2020] and NetLogo [@netlogo:2004]. +These have a different scope than `Mango.jl`, which is more focused on agent communication and internal agent logic for software applications. JADE [@JADE:2001] and JIAC [@jiac:2013] are Java frameworks of similar scope but are not actively developed anymore. JACK [@jack:2005] provides a language and tools to implement communicating agents but is discontinued and proprietary. @@ -82,23 +82,21 @@ Lastly, the original Python version of mango [@schrage:2024] is of course most s The performance of the Python and Julia versions of mango were benchmarked against each other. The results are shown in \autoref{fig:benchmark} and the relevant code is available at [mango_benchmark](https://github.com/OFFIS-DAI/mango_benchmark). -The aim of these scenarios is to measure the performance of the frameworks core features. +The aim of these scenarios is to measure the performance of the frameworks' core features. This mainly means it measures how efficiently tasks are scheduled and messages are sent and handled. To achieve this, benchmark scenarios have agents set up in a small world topology communicating a fixed number of messages between each other while performing simulated workloads. -All workloads in the agents is entirely simulated by static delays. -Thus, the benchmarks assumes that workloads in Python and Julia are identical. +All workloads in the agents are entirely simulated by static delays. +Thus, the benchmarks assume that workloads in Python and Julia are identical. The main advantage of `Mango.jl` is in the ease of parallelization. -Python can in some cases reach similar performance using subprocesses for parallel execution to circumvent the limitations of the Python global interpreter lock. -Compared to native threads in Julia, however, this is more prone to issues with the operating system, because it requires large amounts of file handles to operate the subprocesses. +Python can, in some cases, reach similar performance using subprocesses for parallel execution to circumvent the limitations of the Python global interpreter lock. +Compared to native threads in Julia, however, this is more prone to issues with the operating system, because it requires large numbers of file handles to operate the subprocesses. Overall, it is easier to get high performance from `Mango.jl`. # Basic Example -> **_NOTE:_** All code examples were tested with Mango.jl v0.4.0 -> The version also has the tag `joss_paper` on the repository. -In this example, we define two agents in two containers (i.e. at different addresses) that pass messages to each other directly via TCP. +In this example, we define two agents in two containers (i.e., at different addresses) that pass messages to each other directly via TCP. Containers can be set up and equipped with the necessary TCP protocol. ```julia @@ -131,7 +129,7 @@ register(container, ping_agent, "Agent_1") register(container2, pong_agent, "Agent_2") ``` -When an incoming message is addressed at an agent, its container will call the `handle_message` function for it. +When an incoming message is addressed to an agent, its container will call the `handle_message` function for it. Using Julia's multiple dispatch, we can define a new `handle_message` method for our agent. ```julia @@ -140,7 +138,7 @@ function Mango.handle_message(agent::TCPPingPongAgent, message::Any, meta::Any) agent.counter += 1 println( - "$(agent.aid) got a message: $message." * + "$(aid(agent)) got a message: $message." * "This is message number: $(agent.counter) for me!" ) diff --git a/src/Mango.jl b/src/Mango.jl index 9feab4ec..ec35f4b9 100644 --- a/src/Mango.jl +++ b/src/Mango.jl @@ -9,22 +9,26 @@ include("util/scheduling.jl") include("util/encode_decode.jl") include("agent/api.jl") include("container/api.jl") +include("environment/api.jl") include("agent/role.jl") include("agent/core.jl") -include("world/core.jl") +include("agent/services.jl") +include("environment/core.jl") include("container/protocol.jl") include("container/tcp.jl") include("container/mqtt.jl") include("simulation/communication.jl") include("simulation/tasks.jl") -include("container/simulation.jl") include("container/core.jl") -include("world/topology.jl") +include("simulation/container.jl") +include("simulation/world.jl") +include("util/topology.jl") include("express/api.jl") +include("visualization.jl") end # module diff --git a/src/agent/api.jl b/src/agent/api.jl index 9c6b8226..8cd1c1c8 100644 --- a/src/agent/api.jl +++ b/src/agent/api.jl @@ -1,4 +1,4 @@ -export Agent, send_message, send_tracked_message, reply_to, address, aid, send_and_handle_answer +export Agent, send_message, send_tracked_message, reply_to, address, aid, send_and_handle_answer, MessagePreprocessor, WaitingMessagePreprocessor """ @@ -19,7 +19,13 @@ implementations across all agents. """ abstract type Agent <: AgentInterface end -function subscribe_message_handle(agent::AgentInterface, role::Any, condition::Any, handler::Any) end +abstract type MessagePreprocessor end + +function init(preprocessor::MessagePreprocessor, role::Any) end +function handle(preprocessor::MessagePreprocessor, message::Any, meta::AbstractDict) end +function process(preprocessor::MessagePreprocessor, message::Any, meta::AbstractDict) end + +function subscribe_message_handle(agent::AgentInterface, role::Any, condition::Any, handler::Any, preprocessor::Union{Nothing,MessagePreprocessor}=nothing) end """ subscribe_send_handle(agent::AgentInterface, role::Any, handler::Any) diff --git a/src/agent/core.jl b/src/agent/core.jl index 8f6660e5..beccaaaa 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -1,7 +1,9 @@ export @agent, AgentContext, AgentRoleHandler, + SystemHandler, handle_message, + handle_unanswered, add, schedule, stop_and_wait_for_all_tasks, @@ -15,7 +17,21 @@ export @agent, ForwardingRule, service_of_type, add_service!, - services + services, + on_global_event, + sender_address, + send_and_handle_answers, + send_tracked_messages, + send_messages, + has_role, + description, + name, + color, + category, + update_description, + AgentDescription, + uid, + uuid4 using UUIDs @@ -31,6 +47,7 @@ for the agent. """ struct AgentContext container::ContainerInterface + evironment::Environment end """ @@ -38,7 +55,7 @@ Internal data regarding the roles. """ struct AgentRoleHandler roles::Vector{Role} - handle_message_subs::Vector{Tuple{Role,Function,Function}} + handle_message_subs::Vector{Tuple{Role,Function,Function,Union{Nothing,MessagePreprocessor}}} send_message_subs::Vector{Tuple{Role,Function}} event_subs::Dict{Any,Vector{Tuple{Role,Function,Function}}} models::Dict{DataType,Any} @@ -50,6 +67,20 @@ struct ForwardingRule forward_replies::Bool end +mutable struct AgentDescription + aid::Union{Nothing,String} + name::String + category::Symbol + color::Symbol + uid::UUID +end + +struct SystemHandler + message_subs::Vector{Tuple{Function,Function,Union{Nothing,MessagePreprocessor},Any}} + event_subs::Dict{Any,Vector{Tuple{Function,Function,Any}}} + global_event_subs::Vector{Tuple{Function,Function,Any}} +end + """ All baseline fields added by the @agent macro are listed in this vector. They are added in the same order defined here. @@ -57,11 +88,13 @@ They are added in the same order defined here. AGENT_BASELINE_FIELDS::Vector = [ :(lock::ReentrantLock = ReentrantLock()), :(context::Union{Nothing,AgentContext} = nothing), - :(role_handler::Union{AgentRoleHandler} = AgentRoleHandler(Vector(), Vector(), Vector(), Dict(), Dict())), + :(role_handler::AgentRoleHandler = AgentRoleHandler(Vector(), Vector(), Vector(), Dict(), Dict())), + :(system_handler::SystemHandler = SystemHandler(Vector(), Dict(), Vector())), :(scheduler::AbstractScheduler = Scheduler()), - :(aid::Union{Nothing,String} = nothing), :(transaction_handler::Dict{String,Tuple} = Dict{String,Tuple}()), :(forwarding_rules::Vector{ForwardingRule} = Vector{ForwardingRule}()), + :(outgoing::Vector{Tuple} = Vector{Tuple}()), + :(description::AgentDescription = AgentDescription(nothing, "", :agent, :gray, uuid4())), :(services::Dict{DataType,Any} = Dict{DataType,Any}()) ] @@ -120,11 +153,63 @@ macro agent(struct_def) esc(Expr(:block, new_struct_def)) end +Base.show(io::IO, p::Agent) = print(io, "Agent $(aid(p))") function build_forwarded_address_from_meta(meta::AbstractDict) return AgentAddress(aid=meta["reply_to_forwarded_from_id"], address=meta["reply_to_forwarded_from_address"], tracking_id=get(meta, TRACKING_ID, nothing)) end +function handle_transaction_message(agent::Agent, message::Any, meta::AbstractDict) + caller, response_handler, addrs, msgs, metas = agent.transaction_handler[meta[TRACKING_ID]] + sender = sender_address_tracked(meta) + if length(addrs) == 1 + if addrs[1] == sender + push!(msgs, message) + push!(metas, meta) + delete!(agent.transaction_handler, meta[TRACKING_ID]) + if length(msgs) == 1 + response_handler(caller, msgs[1], metas[1]) + else + response_handler(caller, msgs, metas) + end + else + @warn "The transaction $(meta[TRACKING_ID]) seems to be polluted, no incoming message from $sender expected!" aid(agent) addrs message msgs + end + else + # length(addrs) always > 0 -> otherwise sending the message would fail in first place. + deleting = findall(x->x==sender, addrs) + if length(deleting) != 0 + push!(msgs, message) + push!(metas, meta) + deleteat!(addrs, deleting) + else + @warn "The transaction $(meta[TRACKING_ID]) seems to be polluted, no incoming message from $sender expected!" aid(agent) addrs message msgs + end + end +end + +@kwdef struct WaitingMessagePreprocessor <: MessagePreprocessor + waiting_for_func::Function + waiting::Dict{AgentAddress,Bool} = Dict() +end + +function init(preprocessor::WaitingMessagePreprocessor, role_or_agent::Union{Role, Agent}) + for addr in preprocessor.waiting_for_func() + preprocessor.waiting[addr] = true + end +end + +function handle(preprocessor::WaitingMessagePreprocessor, role_or_agent::Union{Role, Agent}, handler::Function, message::Any, meta::AbstractDict) + sender = sender_address(meta) + if sender in keys(preprocessor.waiting) + preprocessor.waiting[sender] = false + end + if !any(values(preprocessor.waiting)) + init(preprocessor, role_or_agent) + handler(role_or_agent, message, meta) + end +end + """ Internal API used by the container to dispatch an incoming message to the agent. In this function the message will be handed over to the different handlers in the @@ -147,29 +232,73 @@ function dispatch_message(agent::Agent, message::Any, meta::AbstractDict) end end if forwarded + agent.outgoing = [] return end lock(agent.lock) do # check if part of a transaction - if haskey(meta, TRACKING_ID) && haskey(agent.transaction_handler, meta[TRACKING_ID]) - caller, response_handler = agent.transaction_handler[meta[TRACKING_ID]] - delete!(agent.transaction_handler, meta[TRACKING_ID]) - response_handler(caller, message, meta) + if haskey(meta, TRACKING_ID) && + haskey(agent.transaction_handler, meta[TRACKING_ID]) && + haskey(meta, "reply") + + handle_transaction_message(agent, message, meta) else for role in agent.role_handler.roles handle_message(role, message, meta) end - for (role, call, condition) in agent.role_handler.handle_message_subs - if condition(message, meta) - call(role, message, meta) + for (role, call, condition, preprocessor) in agent.role_handler.handle_message_subs + if isnothing(preprocessor) + if condition(message, meta) + call(role, message, meta) + end + else + if condition(message, meta) + handle(preprocessor, role, call, message, meta) + end end end handle_message(agent, message, meta) + for (condition, call, preprocessor, caller) in agent.system_handler.message_subs + if isnothing(preprocessor) + if condition(message, meta) + call(caller, message, meta) + end + else + if condition(message, meta) + handle(preprocessor, agent, call, message, meta) + end + end + end end + if length(agent.outgoing) < 1 + for role in agent.role_handler.roles + handle_unanswered(role, message, meta) + end + handle_unanswered(agent, message, meta) + end + agent.outgoing = [] end end +""" + sender_address(meta::Any) + +Extract the sender address from the meta data of a message and return it as `AgentAddress`. +""" +function sender_address(meta::AbstractDict) + return AgentAddress(aid=meta[SENDER_ID], address=meta[SENDER_ADDR]) +end + +""" + sender_address(meta::Any) + +Extract the sender address from the meta data of a message and return it as `AgentAddress`. +""" +function sender_address_tracked(meta::AbstractDict) + return AgentAddress(aid=meta[SENDER_ID], address=meta[SENDER_ADDR], tracking_id=haskey(meta, TRACKING_ID) ? meta[TRACKING_ID] : nothing) +end + """ handle_message(agent::Agent, message::Any, meta::Any) @@ -181,6 +310,17 @@ function handle_message(agent::Agent, message::Any, meta::Any) # do nothing by default end +""" + handle_unanswered(agent::Agent, message::Any, meta::Any) + +Defines a function for an agent, which will be called when after a message has been handled + without any messages sent while handling. Useful to do something when a incoming message is + unknown/ensure there is always an answer. +""" +function handle_unanswered(agent::Agent, message::Any, meta::Any) + # do nothing by default +end + function notify_start(agent::Agent) on_start(agent) for role in roles(agent) @@ -217,8 +357,40 @@ function on_ready(agent::Agent) # do nothing by default end +function description(agent::Agent) + return agent.description +end + function aid(agent::Agent) - return agent.aid + return description(agent).aid +end + +function name(agent::Agent) + return description(agent).name +end + +function category(agent::Agent) + return description(agent).category +end + +function color(agent::Agent) + return description(agent).color +end + +function uid(agent::Agent) + return description(agent).uid +end + +function update_description(agent::Agent; color::Union{Nothing, Symbol}=nothing, name::Union{Nothing, String}=nothing, category::Union{Nothing, Symbol}=nothing) + if !isnothing(name) + description(agent).name = name + end + if !isnothing(color) + description(agent).color = color + end + if !isnothing(category) + description(agent).category = category + end end """ @@ -243,6 +415,15 @@ function roles(agent::Agent) return agent.role_handler.roles end +function has_role(agent::Agent, role_type::DataType) + for role in roles(agent) + if role_type == typeof(role) + return true + end + end + return false +end + """ shutdown(agent) @@ -261,9 +442,25 @@ function subscribe_message_handle( agent::Agent, role::Role, condition::Function, - handler::Function, + handler::Function; + preprocessor::Union{Nothing,MessagePreprocessor}=nothing, +) + if !isnothing(preprocessor) + init(preprocessor, role) + end + push!(agent.role_handler.handle_message_subs, (role, condition, handler, preprocessor)) +end + +function subscribe_message( + agent::Agent, + condition::Function, + handler::Function; + preprocessor::Union{Nothing,MessagePreprocessor}=nothing, ) - push!(agent.role_handler.handle_message_subs, (role, condition, handler)) + if !isnothing(preprocessor) + init(preprocessor, agent) + end + _add_system_handle_message_sub(agent, agent, condition, handler; preprocessor=preprocessor) end function subscribe_send_handle(agent::Agent, role::Role, handler::Function) @@ -289,6 +486,13 @@ function emit_event_handle(agent::Agent, src::Role, event::Any; event_type::Any= for role in roles(agent) handle_event(role, src, event, event_type=event_type) end + if haskey(agent.system_handler.event_subs, key) + for (condition, func, caller) in agent.system_handler.event_subs[key] + if condition(src, event) + func(caller, src, event, event_type) + end + end + end end function get_model_handle(agent::Agent, type::DataType) @@ -335,6 +539,15 @@ function schedule(f::Function, agent::Agent, data::TaskData) schedule(f, agent.scheduler, data) end +""" + clock(agent::Agent) + +Return clock of the agent. +""" +function clock(agent::Agent) + return clock(agent.scheduler) +end + """ stop_and_wait_for_all_tasks(agent::Agent) @@ -379,22 +592,39 @@ function address(agent::Agent) return AgentAddress(aid=aid(agent), address=addr) end -function send_message( +function send_messages( agent::Agent, content::Any, - agent_adress::AgentAddress; + agent_addresses::Vector{AgentAddress}; kwargs..., ) + push!(agent.outgoing, (content, kwargs)) + for (role, handler) in agent.role_handler.send_message_subs - handler(role, content, agent_adress; kwargs...) + for agent_address in agent_addresses + handler(role, content, agent_address; kwargs...) + end end - return send_message( - agent.context.container, - content, - agent_adress, - agent.aid; - kwargs..., - ) + tasks = [] + for agent_address in agent_addresses + push!(tasks, send_message( + agent.context.container, + content, + agent_address, + aid(agent); + kwargs..., + )) + end + return tasks +end + +function send_message( + agent::Agent, + content::Any, + agent_address::AgentAddress; + kwargs..., +) + return send_messages(agent, content, [agent_address]; kwargs...)[1] end function send_message( @@ -414,26 +644,49 @@ function send_message( ) end -function send_tracked_message( +function send_tracked_messages( agent::Agent, content::Any, - agent_address::AgentAddress; + agent_addresses::Vector{AgentAddress}; response_handler::Union{Function,Nothing}=nothing, calling_object::Any=nothing, kwargs..., ) - tracking_id = string(uuid1()) - if !isnothing(agent_address.tracking_id) - tracking_id = agent_address.tracking_id + tracking_id = string(uuid4()) + if !isnothing(agent_addresses[1].tracking_id) + tracking_id = agent_addresses[1].tracking_id end + addrs = [AgentAddress(addr.aid, addr.address, tracking_id) for addr in agent_addresses] if !isnothing(response_handler) caller = agent if !isnothing(calling_object) caller = calling_object end - agent.transaction_handler[tracking_id] = (caller, response_handler) + agent.transaction_handler[tracking_id] = (caller, response_handler, addrs, [], []) end - return send_message(agent, content, AgentAddress(agent_address.aid, agent_address.address, tracking_id); kwargs...) + return send_messages(agent, content, addrs; kwargs...) +end + +function send_tracked_message( + agent::Agent, + content::Any, + agent_address::AgentAddress; + response_handler::Union{Function,Nothing}=nothing, + calling_object::Any=nothing, + kwargs..., +) + return send_tracked_messages(agent, content, [agent_address]; response_handler=response_handler, calling_object=calling_object, kwargs...)[1] +end + +function send_and_handle_answers( + response_handler::Function, + agent::Agent, + content::Any, + agent_addresses::Vector{AgentAddress}; + calling_object::Any=nothing, + kwargs...) + return send_tracked_messages(agent, content, agent_addresses; response_handler=response_handler, + calling_object=calling_object, kwargs...) end function send_and_handle_answer( @@ -443,8 +696,7 @@ function send_and_handle_answer( agent_address::AgentAddress; calling_object::Any=nothing, kwargs...) - return send_tracked_message(agent, content, agent_address; response_handler=response_handler, - calling_object=calling_object, kwargs...) + return send_and_handle_answers(response_handler, agent, content, [agent_address]; calling_object=calling_object, kwargs...)[1] end function reply_to(agent::Agent, @@ -522,4 +774,47 @@ Return the `index`'th role of the agent. """ function Base.getindex(agent::T, index::Int) where {T<:Agent} return roles(agent)[index] +end + +function Base.getindex(agent::T, index::Type) where {T<:Agent} + for role in roles(agent) + if typeof(role) == index + return role + end + end + throw(ArgumentError("The agent has no role of the type index=$index.")) +end + +""" + on_global_event(agent::Agent, event::Any) + +Handle global event. See [`emit_global_event`](@ref). +""" +function on_global_event(agent::Agent, event::Any) + # to be overridden +end + +function dispatch_global_event(agent::Agent, event::Any) + on_global_event(agent, event) + for role in roles(agent) + on_global_event(role, event) + end + for (condition, call, caller) in agent.system_handler.global_event_subs + if condition(event) + call(caller, event) + end + end +end + +function _add_system_handle_message_sub(agent::Agent, caller::Any, filter::Function, handle::Function; preprocessor::Union{Nothing,<:MessagePreprocessor}=nothing) + push!(agent.system_handler.message_subs, (filter, handle, preprocessor, caller)) +end + +function _add_system_event_sub(agent::Agent, caller::Any, event_type::Any, filter::Function, handle::Function) + event_type_subs = get!(agent.system_handler.event_subs, event_type, Vector()) + push!(event_type_subs, (filter, handle, caller)) +end + +function _add_system_global_event_sub(agent::Agent, caller::Any, filter::Function, handle::Function) + push!(agent.system_handler.global_event_subs, (filter, handle, caller)) end \ No newline at end of file diff --git a/src/agent/role.jl b/src/agent/role.jl index abfaf1b3..5835c2c7 100644 --- a/src/agent/role.jl +++ b/src/agent/role.jl @@ -9,7 +9,8 @@ export Role, emit_event, get_model, subscribe_event, - setup + setup, + on_global_event """ @@ -154,6 +155,10 @@ function handle_message(role::Role, message::Any, meta::Any) # do nothing by default end +function handle_unanswered(role::Role, message::Any, meta::Any) + # do nothing by default +end + """ handle_event(role::Role, src::Role, event::Any; event_type::Any) @@ -204,8 +209,8 @@ to the message dispatching. This handler function will be called everytime the g condition function ((message, meta) -> boolean) evaluates to true when a message arrives at the roles agent. """ -function subscribe_message(role::Role, handler::Function, condition::Function) - subscribe_message_handle(role.context.agent, role, handler, condition) +function subscribe_message(role::Role, handler::Function, condition::Function; preprocessor::Union{Nothing,MessagePreprocessor}=nothing) + subscribe_message_handle(role.context.agent, role, handler, condition, preprocessor=preprocessor) end """ @@ -274,10 +279,30 @@ function schedule(f::Function, role::Role, data::TaskData) schedule(f, role.context.agent, data) end +function clock(role::Role) + clock(role.context.agent) +end + function aid(role::Role) return address(role.context.agent).aid end +function description(role::Role) + return description(role.context.agent) +end + +function name(role::Role) + return name(role.context.agent) +end + +function category(role::Role) + return category(role.context.agent) +end + +function color(role::Role) + return color(role.context.agent) +end + function address(role::Role) return address(role.context.agent) end @@ -299,6 +324,14 @@ function send_message( return send_message(role.context.agent, content, agent_adress; kwargs...) end +function send_messages( + role::Role, + content::Any, + agent_adresses::Vector{AgentAddress}; + kwargs..., +) + return send_message(role.context.agent, content, agent_adresses; kwargs...) +end function send_tracked_message( role::Role, @@ -310,6 +343,16 @@ function send_tracked_message( return send_tracked_message(role.context.agent, content, agent_adress; response_handler=response_handler, calling_object=role, kwargs...) end +function send_tracked_messages( + role::Role, + content::Any, + agent_adresses::Vector{AgentAddress}; + response_handler::Function=(role, message, meta) -> nothing, + kwargs..., +) + return send_tracked_messages(role.context.agent, content, agent_adresses; response_handler=response_handler, calling_object=role, kwargs...) +end + function send_and_handle_answer( response_handler::Function, role::Role, @@ -320,10 +363,20 @@ function send_and_handle_answer( calling_object=role, kwargs...) end +function send_and_handle_answers( + response_handler::Function, + role::Role, + content::Any, + agent_addresses::Vector{AgentAddress}; + kwargs...) + return send_and_handle_answers(response_handler, role.context.agent, content, agent_addresses; + calling_object=role, kwargs...) +end + function reply_to(role::Role, content::Any, received_meta::AbstractDict; - response_handler::Function=(agent, message, meta) -> nothing, + response_handler::Union{Nothing,Function}=nothing, kwargs...) return reply_to(role.context.agent, content, received_meta; response_handler=response_handler, calling_object=role, kwargs...) end @@ -335,3 +388,12 @@ function forward_to(role::Role, kwargs...) return forward_to(role.context.agent, content, forward_to_address, received_meta; kwargs...) end + +""" + on_global_event(role::Role, event::Any) + +Handle global event. See [`emit_global_event`](@ref). +""" +function on_global_event(role::Role, event::Any) + # to be overridden +end diff --git a/src/agent/services.jl b/src/agent/services.jl new file mode 100644 index 00000000..e7f4aff5 --- /dev/null +++ b/src/agent/services.jl @@ -0,0 +1,51 @@ +export observation, actions, action, install_action, install_observer + +mutable struct ObservationService + observers::Dict{Symbol,Function} +end + +function observe(observation_service::ObservationService, symbol::Symbol) + return observation_service.observers[symbol]() +end + +function observation(agent::Agent, symbol::Symbol=:default)::Any + return observe(service_of_type(agent, ObservationService, ObservationService(Dict())), symbol) +end + +function observation(role::Role, symbol::Symbol=:default)::Any + return observation(role.context.agent, symbol) +end + +function install_observer(observer::Function, agent::Agent, symbol::Symbol=:default) + s = service_of_type(agent, ObservationService, ObservationService(Dict())) + s.observers[symbol] = observer +end + +struct ActionService + actions::Dict{Symbol,Function} +end + +function actions(action_service::ActionService) + return action_service.actions +end + +function actions(agent::Agent)::Dict{Symbol,Function} + return actions(service_of_type(agent, ActionService, ActionService(Dict()))) +end + +function action(agent::Agent, symbol::Symbol)::Function + return actions(agent)[symbol] +end + +function actions(role::Role)::Dict{Symbol,Function} + return actions(role.context.agent) +end + +function action(role::Role, symbol::Symbol)::Function + return actions(role)[symbol] +end + +function install_action(action::Function, agent::Agent, symbol::Symbol) + s = service_of_type(agent, ActionService, ActionService(Dict())) + s.actions[symbol] = action +end \ No newline at end of file diff --git a/src/container/api.jl b/src/container/api.jl index 50be1e79..1ac0583f 100644 --- a/src/container/api.jl +++ b/src/container/api.jl @@ -107,13 +107,6 @@ function register( kwargs..., ) end -""" - agents(container) - -Return the agents of the container. The agents have a fixed order. -""" -function agents(container::ContainerInterface) end - """ notify_ready(container::Container) diff --git a/src/container/core.jl b/src/container/core.jl index 8983b391..0383c70a 100644 --- a/src/container/core.jl +++ b/src/container/core.jl @@ -96,6 +96,7 @@ function register( container::Container, agent::Agent, suggested_aid::Union{String,Nothing}=nothing; + env::Environment=NoEnv(), kwargs..., ) actual_aid::String = "$AGENT_PREFIX$(container.agent_counter)" @@ -103,8 +104,8 @@ function register( actual_aid = suggested_aid end container.agents[actual_aid] = agent - agent.aid = actual_aid - agent.context = AgentContext(container) + description(agent).aid = actual_aid + agent.context = AgentContext(container, env) container.agent_counter += 1 if !isnothing(container.protocol) diff --git a/src/container/mqtt.jl b/src/container/mqtt.jl index fadff105..134574aa 100644 --- a/src/container/mqtt.jl +++ b/src/container/mqtt.jl @@ -51,7 +51,7 @@ Initialize the Mosquitto looping task for the provided `protocol` and forward in function init(protocol::MQTTProtocol, stop_check::Function, data_handler::Function) tasks = [] listen_task = errormonitor( - Threads.@spawn begin + @spawnlog begin try run_mosquitto_loop(protocol, data_handler) catch err diff --git a/src/container/simulation.jl b/src/container/simulation.jl deleted file mode 100644 index b1aaca27..00000000 --- a/src/container/simulation.jl +++ /dev/null @@ -1,384 +0,0 @@ -export SimulationContainer, register, send_message, shutdown, protocol_addr, create_simulation_container, step_simulation, SimulationResult, CommunicationSimulationResult, TaskSimulationResult, on_step - -using Base.Threads -using Dates -using ConcurrentCollections -using OrderedCollections - -""" -Id key for the receiver in the meta dict -""" -RECEIVER_ID::String = "receiver_id" -""" -Prefix for the generated aid's -""" -AGENT_PREFIX::String = "agent" -""" -DISCRETE EVENT STEP SIZE -""" -DISCRETE_EVENT::Real = -1 - -""" - create_simulation_container(start_time::DateTime; communication_sim::Union{Nothing,CommunicationSimulation}=nothing, task_sim::Union{Nothing,TaskSimulation}=nothing) - -Create a simulation container. The container is intitialized with `start_time`. - -Per default the [`SimpleCommunicationSimulation`](@ref) is used for communication simulation, and -[`SimpleTaskSimulation`](@ref) for simulating the tasks of agents. To replace these, `communication_sim` -and respectively `task_sim` can be set. -""" -function create_simulation_container(start_time::DateTime; communication_sim::Union{Nothing,CommunicationSimulation}=nothing, task_sim::Union{Nothing,TaskSimulation}=nothing) - container = SimulationContainer() - container.clock.simulation_time = start_time - if !isnothing(communication_sim) - container.communication_sim = communication_sim - end - if !isnothing(task_sim) - container.task_sim = task_sim - end - return container -end - -""" -Represents a message data package including the arriving time of the package. -""" -struct MessageData - content::Any - meta::AbstractDict - arriving_time::DateTime -end - -""" -The SimulationContainer used as a base struct to enable simulations in Mango.jl. Shall be created -using [`create_simulation_container`](@ref). -""" -@kwdef mutable struct SimulationContainer <: ContainerInterface - world::World = World() - clock::Clock = Clock(DateTime(0)) - task_sim::TaskSimulation = SimpleTaskSimulation(clock=clock) - agents::OrderedDict{String,Agent} = OrderedDict() - agent_counter::Integer = 0 - shutdown::Bool = false - communication_sim::CommunicationSimulation = SimpleCommunicationSimulation() - message_queue::ConcurrentQueue{MessageData} = ConcurrentQueue{MessageData}() -end - -function agents(container::SimulationContainer)::Vector{Agent} - return [t[2] for t in collect(container.agents)] -end - -""" - on_step(agent::Agent, world::World, clock::Clock, step_size_s::Real) - -Hook-in, called on every step of the simulation container for every `agent`. - -Further, the `world` is passed, which represents a common view on the environment -in which agents can interact with eachother. Besides, the `clock` and the `step_size_s` -can be used to read the current simulation time and the time which passes in the current step. -""" -function on_step(agent::Agent, world::World, clock::Clock, step_size_s::Real) - # default nothing -end - -function on_step(role::Role, world::World, clock::Clock, step_size_s::Real) - # default nothing -end - -""" -Internal, call on_step on all agents. -""" -function step_agent(agent::Agent, world::World, clock::Clock, step_size_s::Real) - on_step(agent, world, clock, step_size_s) - for role in roles(agent) - on_step(role, world, clock, step_size_s) - end -end - -""" -Contains the result of the communication simulation and whether the state of -the container has changed -""" -struct MessagingIterationResult - communication_result::CommunicationSimulationResult - state_changed::Bool -end - -""" -Result of all messaging simulation iterations. -""" -@kwdef struct MessagingSimulationResult - results::Vector{MessagingIterationResult} = Vector() -end - -""" -Result of all task simulation iterations. -""" -@kwdef struct TaskSimulationResult - results::Vector{TaskIterationResult} = Vector() -end - -""" -Result of one simulation step. -""" -struct SimulationResult - time_elapsed::Real - messaging_result::MessagingSimulationResult - task_result::TaskSimulationResult - simulation_step_size_s::Real -end - -""" -Internal -""" -function to_message_package(message_data::MessageData)::MessagePackage - sender_aid = message_data.meta[SENDER_ID] - receiver_aid = message_data.meta[RECEIVER_ID] - return MessagePackage(sender_aid, receiver_aid, message_data.arriving_time, (message_data.content, message_data.meta)) -end - -""" -Internal -""" -function to_cs_input!(message_queue::ConcurrentQueue{MessageData})::Vector{MessagePackage} - messages_packages = Vector() - while true - some_message = maybepopfirst!(message_queue) - if isnothing(some_message) - break - end - message::MessageData = something(some_message) - push!(messages_packages, to_message_package(message)) - end - return messages_packages -end - -""" -Internal -""" -function to_cs_input(message_queue::ConcurrentQueue{MessageData})::Vector{MessagePackage} - messages_packages = Vector() - next = message_queue.head.next - while !isnothing(next) - message::MessageData = next.value - push!(messages_packages, to_message_package(message)) - next = next.next - end - return messages_packages -end - -""" -Internal -""" -function cs_step_iteration(container::SimulationContainer, - step_size_s::Real, - pre_communication_result::Union{Nothing,CommunicationSimulationResult})::MessagingIterationResult - message_packages = to_cs_input!(container.message_queue) - communication_result = pre_communication_result - if isnothing(communication_result) - communication_result = calculate_communication(container.communication_sim, - container.clock, - message_packages) - end - state_changed = false - @sync begin - for (mp, pr) in sort([z for z in zip(message_packages, communication_result.package_results)], by=t -> add_seconds(t[1].sent_date, t[2].delay_s)) - if add_seconds(mp.sent_date, pr.delay_s) <= add_seconds(container.clock.simulation_time, step_size_s) && pr.reached - state_changed = true - @spawnlog process_message(container, mp.content[1], mp.content[2]) - else - # process it later - push!(container.message_queue, MessageData(mp.content[1], mp.content[2], mp.sent_date)) - end - end - end - return MessagingIterationResult(communication_result, state_changed) -end - -""" -Internal -""" -function determine_time_step(container::SimulationContainer) - message_packages = to_cs_input(container.message_queue) - communication_result = calculate_communication(container.communication_sim, container.clock, message_packages) - - # earliest message or -1 if no message arrives - message_arrival_times = [add_seconds(t[1].sent_date, t[2].delay_s) for t in zip(message_packages, communication_result.package_results)] - time_to_next_message_s = nothing - if length(message_arrival_times) > 0 - time_to_next_message_s = (findmin(message_arrival_times)[1] - container.clock.simulation_time).value / 1000 - end - @debug "Next message in $time_to_next_message_s" - - # ealiest task or -1 if no task scheduled - next_event_s = determine_next_event_time(container.task_sim) - - @debug "Next event in $next_event_s" - - # check whether one is absent and the other is present - if isnothing(time_to_next_message_s) && isnothing(next_event_s) - return nothing, communication_result - elseif isnothing(next_event_s) - return time_to_next_message_s, communication_result - elseif isnothing(time_to_next_message_s) - return next_event_s, communication_result - end - - # return earliest - return min(time_to_next_message_s, next_event_s), communication_result -end - -""" - step_simulation(container::SimulationContainer, step_size_s::Real=DISCRETE_EVENT)::Union{SimulationResult,Nothing} - -Step the simulation using a continous time-span or until the next event happens. - -For the continous simulation a `step_size_s` can be freely chosen, for the discrete event type -DISCRETE_EVENT has to be set for the `step_size_s`. -""" -function step_simulation(container::SimulationContainer, step_size_s::Real=DISCRETE_EVENT)::Union{SimulationResult,Nothing} - # Init world if uninitialized - if !initialized(container.world) - initialize(container.world, [v for v in values(container.agents)]) - end - - state_changed = true - - @debug "Time" container.clock - - task_sim_result = TaskSimulationResult() - messaging_sim_result = MessagingSimulationResult() - first_step = true - time_step_s = step_size_s - - # We are in discrete event mode, so we need to determine - # the time until the next event occurs, this time will - # be used to execute the time-based simulation - comm_result = nothing - if time_step_s == DISCRETE_EVENT - time_step_s, comm_result = determine_time_step(container) - @debug "Determined the size to be $time_step_s" - if isnothing(time_step_s) - return nothing - end - end - elapsed = @elapsed begin - # now we process everything which happened in the steps, - # tasks and previous iterations - while state_changed - @debug "Start simulation iteration" - task_iter_result = nothing - comm_iter_result = nothing - @sync begin - Threads.@spawn comm_iter_result = cs_step_iteration(container, time_step_s, first_step ? comm_result : nothing) - Threads.@spawn task_iter_result = step_iteration(container.task_sim, time_step_s, first_step) - end - first_step = false - push!(task_sim_result.results, task_iter_result) - push!(messaging_sim_result.results, comm_iter_result) - state_changed = comm_iter_result.state_changed || task_iter_result.state_changed - @debug "Finish simulation iteration" state_changed - end - - # agents act on the stepping hook - for agent in values(container.agents) - step_agent(agent, container.world, container.clock, time_step_s) - end - end - @debug "The simulation step needed $elapsed seconds" - - container.clock.simulation_time = add_seconds(container.clock.simulation_time, time_step_s) - - @debug "new time", container.clock.simulation_time - - return SimulationResult(elapsed, messaging_sim_result, task_sim_result, time_step_s) -end - -function protocol_addr(container::SimulationContainer) - return nothing -end - -function shutdown(container::SimulationContainer) - container.shutdown = true - - for agent in values(container.agents) - shutdown(agent) - end -end - -function register( - container::SimulationContainer, - agent::Agent, - suggested_aid::Union{String,Nothing}=nothing; - kwargs..., -) - actual_aid::String = "$AGENT_PREFIX$(container.agent_counter)" - if !isnothing(suggested_aid) && !haskey(container.agents, suggested_aid) - actual_aid = suggested_aid - end - container.agents[actual_aid] = agent - agent.aid = actual_aid - agent.context = AgentContext(container) - container.agent_counter += 1 - - if !isnothing(container.task_sim) - agent.scheduler = create_agent_scheduler(container.task_sim) - end - - return agent -end - -function process_message(container::SimulationContainer, msg::Any, meta::AbstractDict) - receiver_id = meta[RECEIVER_ID] - - if !haskey(container.agents, meta[RECEIVER_ID]) - @warn "Container $(keys(container.agents)) has no agent with id: $receiver_id" msg meta - else - agent = container.agents[receiver_id] - return dispatch_message(agent, msg, meta) - end -end - -struct NonWaitable end -function Base.wait(waitable::NonWaitable) end - -function forward_message(container::SimulationContainer, msg::Any, meta::AbstractDict) - push!(container.message_queue, MessageData(msg, meta, container.clock.simulation_time)) - return NonWaitable() -end - -function send_message( - container::SimulationContainer, - content::Any, - agent_adress::AgentAddress, - sender_id::Union{Nothing,String}=nothing; - kwargs..., -) - receiver_id = agent_adress.aid - tracking_id = agent_adress.tracking_id - - meta = OrderedDict{String,Any}() - for (key, value) in kwargs - meta[string(key)] = value - end - - meta[RECEIVER_ID] = receiver_id - meta[SENDER_ID] = sender_id - meta[TRACKING_ID] = tracking_id - meta[SENDER_ADDR] = nothing - - @debug "Send a message to ($receiver_id), from $sender_id" typeof(content) - - return forward_message(container, content, meta) -end - -""" - Base.getindex(container::SimulationContainer, index::String) - -Return the agent indexed by `index` (aid). -""" -function Base.getindex(container::SimulationContainer, index::String) - return container.agents[index] -end -function Base.getindex(container::SimulationContainer, index::Int) - return agents(container)[index] -end \ No newline at end of file diff --git a/src/container/tcp.jl b/src/container/tcp.jl index 026841aa..53fe3101 100644 --- a/src/container/tcp.jl +++ b/src/container/tcp.jl @@ -60,7 +60,7 @@ function close(pool::TCPConnectionPool) pool.closed = true # Waiting until all acquired connections are released - wait(Threads.@spawn begin + wait(@spawnlog begin while pool.acquired_connections.counter > 0 sleep(0.0001) end @@ -229,7 +229,7 @@ function init(protocol::TCPProtocol, stop_check::Function, data_handler::Functio protocol.server = server tasks = [] listen_task = errormonitor( - Threads.@spawn begin + @spawnlog begin try while isopen(server) connection = accept(server) diff --git a/src/environment/api.jl b/src/environment/api.jl new file mode 100644 index 00000000..cbdb0dd3 --- /dev/null +++ b/src/environment/api.jl @@ -0,0 +1,81 @@ +export Position, Space, WorldObserver, Behavior, Environment, NoEnv, install, dispatch_global_event, initialize, initialized, add_observer! + +abstract type Position end +abstract type WorldObserver end +abstract type Behavior end +abstract type Environment end + +struct NoEnv <: Environment end + +function dispatch_global_event(observer::WorldObserver, event::Any) + # default no reaction +end + + +""" + schedule(f::Function, environment::Environment, data::TaskData) + +Schedule a task for the given environment. +""" +function schedule(f::Function, environment::Environment, data::TaskData) + throw("Schedule is not implemented for $environment") +end + +""" + step(environment::Environment, clock::Clock, step_size_s::Real) + +Step the environment for the given time and advancing step_size_s. +""" +function step(environment::Environment, clock::Clock, step_size_s::Real) + throw("Step is not implemented for $environment") +end + +""" + initialize(environment::Environment, agents::Vector{A}) where {A<:Agent} + +Initialize the environment. Should be called once per instantiated Environment. +""" +function initialize(environment::Environment, agents::Vector{A}) where {A<:Agent} + # default do nothing +end + +""" + initialized(environment::Environment) + +Return true, if the environment is already initialized. +""" +function initialized(environment::Environment) + throw("Initialized is not implemented for $environment") +end + +""" + add_observer!(environment::Environment, observer::WorldObserver) + +Add an observer to the environment, which is able to handle +global event emitted by the environment. +""" +function add_observer!(environment::Environment, observer::WorldObserver) + throw("Add observer not implemented for $environment") +end + +""" + emit_global_event(environment::Environment, event::Any) + +Emit an global event. This types of events can be handled by any agent +living in the environment (resp. living in the world, the environment exists in). +Therefore, any of those agents (and roles) can handle event emitted with +this function by defining [`on_global_event`](@ref). +""" +function emit_global_event(environment::Environment, event::Any) + throw("Emit global event not implemented for $environment") +end + +""" + install(environment::Environment, agent::A; kwargs...) where {A<:Agent} + +Install the agent to the environment using optional additional information. This method can +be used to +""" +function install(environment::Environment, agent::A; additional_information...) where {A<:Agent} + throw("Install is not implemented for $environment") +end \ No newline at end of file diff --git a/src/environment/core.jl b/src/environment/core.jl new file mode 100644 index 00000000..86bda07c --- /dev/null +++ b/src/environment/core.jl @@ -0,0 +1,174 @@ +export DefaultEnvironment, Position2D, Area2D, location, + move, initialize, initialized, schedule, + emit_global_event, behavior, space, install + +struct NoBehavior <: Behavior end + +abstract type Space{P<:Position} end + +""" + move(space::Space{P}, agent::Agent, position::P) where {P<:Position} + +Move the `agent` to `position` in `space`. +""" +function move(space::Space{P}, agent::Agent, position::P) where {P<:Position} + throw("Move on the space $space not defined!") +end + + +""" + initialize(space::Space, agents::Vector{A}) where {A<:Agent} + +Initializes the space. +""" +function initialize(space::Space, agents::Vector{A}) where {A<:Agent} + throw("Initialization for $space is not defined!") +end + +""" + install(space::Space{P}, agent::Agent; additional_information...) where {P<:Position} + +Install the agent on the space. +""" +function install(space::Space{P}, agent::Agent; additional_information...) where {P<:Position} + # do nothing by default +end + +""" + location(space::Area2D, agent::Agent)::Position2D + +Return the location of the `agent`. +""" +function location(space::Space{P}, agent::Agent)::P where {P<:Position} + throw("Position on the space $space not defined!") +end + +struct Position2D <: Position + x::Real + y::Real +end + +@kwdef struct Area2D <: Space{Position2D} + width::Real + height::Real + to_position::Dict{String,Position2D} = Dict() +end + +""" +Struct DefaultEnvironment. The environment provides a description of everything which exists outside of the agents. + +The environment is a separate entity, which describes some type of environment, this can be anything which exists in +any type of space, this can be some model/evironment, which is observed by the agents. The agents can interact +with the environment and exist in the defined space. +""" +@kwdef mutable struct DefaultEnvironment{S<:Space} <: Environment + scheduler::SimulationScheduler + space::S = Area2D(width=10, height=10) + behavior::Behavior = NoBehavior() + observers::Vector{WorldObserver} = Vector{WorldObserver}() + initialized::Bool = false +end + +schedule(f::Function, environment::DefaultEnvironment, data::TaskData) = schedule(f, environment.scheduler, data) + +""" + on_step(behavior::Behavior, environment::DefaultEnvironment, clock::Clock, step_size_s::Real) + +Called on stepping the container. +""" +function on_step(behavior::Behavior, environment::DefaultEnvironment, clock::Clock, step_size_s::Real) + # default do nothing +end + +""" + install(behavior::Behavior, agent::Agent; additional_information...) + +Install the agent using the behavior data. +""" +function install(behavior::Behavior, agent::Agent; additional_information...) + # do nothing by default +end + +function step(env::DefaultEnvironment, clock::Clock, step_size_s::Real) + on_step(behavior(env), env, clock, step_size_s) +end + +function location(space::Area2D, agent::Agent)::Position2D + return space.to_position[aid(agent)] +end + +function move(space::Area2D, agent::Agent, position::Position2D) + space.to_position[aid(agent)] = position +end + +function initialize(space::Area2D, agents::Vector{A}) where {A<:Agent} + for agent in agents + space.to_position[aid(agent)] = Position2D(rand() * space.width, rand() * space.height) + end +end + +function initialize(behavior::Behavior) + # default no initialization +end + +function initialize(environment::DefaultEnvironment{S}, agents::Vector{A}) where {S<:Space} where {A<:Agent} + initialize(environment.space, agents) + initialize(behavior(environment)) + environment.initialized = true +end + +""" + initialized(environment::DefaultEnvironment) + +Return whether the environment is intialized. +""" +function initialized(environment::DefaultEnvironment) + return environment.initialized +end + +""" + add_observer!(environment::DefaultEnvironment, observer::WorldObserver) + +Add an observer to the environment, which is able to handle +global event emitted by the environment. +""" +function add_observer!(environment::DefaultEnvironment, observer::WorldObserver) + push!(environment.observers, observer) +end + +""" + behavior(env::DefaultEnvironment) + +Return the behavior of the environment. +""" +function behavior(env::DefaultEnvironment) + return env.behavior +end + +""" + space(env::DefaultEnvironment) + +The space of the environment +""" +function space(env::DefaultEnvironment) + return env.space +end + +""" + emit_global_event(environment::DefaultEnvironment, event::Any) + +Emit a global event. This types of events can be handled by any agent +living in the environment (resp. living in the world, the environment exists in). +Therefore, any of those agents (and roles) can handle events emitted with +this function by defining [`on_global_event`](@ref). +""" +function emit_global_event(environment::DefaultEnvironment, event::Any) + for observer in environment.observers + dispatch_global_event(observer, event) + end +end + +function install(environment::DefaultEnvironment, agent::A; additional_information...) where {A<:Agent} + install(space(environment), agent; additional_information...) + install(behavior(environment), agent; additional_information...) +end diff --git a/src/express/api.jl b/src/express/api.jl index d6065035..a35206a3 100644 --- a/src/express/api.jl +++ b/src/express/api.jl @@ -1,4 +1,4 @@ -export create_tcp_container, create_mqtt_container, GeneralAgent, add_agent_composed_of, agent_composed_of, activate, run_in_real_time, run_in_simulation, run_with_mqtt, run_with_tcp, PrintingAgent +export create_tcp_container, create_mqtt_container, GeneralAgent, add_agent_composed_of, agent_composed_of, activate, run_in_real_time, run_in_simulation, run_with_mqtt, run_with_tcp, PrintingAgent, behavior_in function _set_codec(container::Container, codec::Union{Nothing,Tuple{Function,Function}}) if !isnothing(codec) @@ -312,20 +312,20 @@ end Let the agents run as simulation in a simulation container. -Execute the `runnable` in [`SimulationContainer`](@ref) while the container is active to run. After the +Execute the `runnable` in [`World`](@ref) while the container is active to run. After the runnable the simulation container is stepped `n_steps` time with a `step_size_s` (default is discrete event). The start time can be specified using `start_time`. """ function run_in_simulation(runnable::Function, n_steps::Int, agents::Agent...; start_time::DateTime=DateTime(2000, 1, 1), step_size_s::Int=DISCRETE_EVENT, communication_sim::Union{Nothing,CommunicationSimulation}=nothing) - sim_container = create_simulation_container(start_time, communication_sim=communication_sim) + sim_world = create_world(start_time, communication_sim=communication_sim) for agent in agents - register(sim_container, agent) + register(sim_world, agent) end results = [] - activate(sim_container) do - runnable(sim_container) + activate(sim_world) do + runnable(sim_world) for _ in 1:n_steps - push!(results, step_simulation(sim_container, step_size_s)) + push!(results, step_simulation(sim_world, step_size_s)) end end return results @@ -338,4 +338,88 @@ Simple agent just printing every message to @info. function handle_message(agent::PrintingAgent, message::Any, meta::Any) @info "Got" message, meta -end \ No newline at end of file +end + +function _has_any_role(agent::Agent, role_types::Vector{DataType}) + return any([has_role(agent, role_type) for role_type in role_types]) +end + + +""" + behavior_in(func::Function, world::World; + on_event::Union{Nothing,DataType}=nothing, + on_global_event::Union{Nothing,DataType}=nothing, + on_message::Union{Nothing,DataType}=nothing, + agent_types::Union{Vector{DataType},DataType}=Vector(), + has_roles::Union{Vector{DataType},DataType}=Vector(), + match_names::Union{Vector{String},String}=Vector(), + match_colors::Union{Vector{String},String}=Vector()) + +Create a behavior for the matching agents. The agent is matched to the agent types, its roles, names, and colors. +This attributes are only checked if provided. If no matching is provided the behavior will be valid for all agents. +The behavior unifies the handling of global_events, agent events and message handles. Each type can be matched using the +event/message type. +""" +function behavior_in(func::Function, world::World; + on_event::Union{Nothing,DataType}=nothing, + on_global_event::Union{Nothing,DataType}=nothing, + on_message::Union{Nothing,DataType}=nothing, + agent_types::Union{Vector{DataType},DataType}=Vector{DataType}(), + role_types::Union{Vector{DataType},DataType}=Vector{DataType}(), + has_roles::Union{Vector{DataType},DataType}=Vector{DataType}(), + match_names::Union{Vector{String},String}=Vector{String}(), + match_colors::Union{Vector{String},String}=Vector{String}(), + preprocessor::Union{Nothing,MessagePreprocessor}=nothing) + + if !(agent_types isa Vector) + agent_types = [agent_types] + end + if !(role_types isa Vector) + role_types = [role_types] + end + if !(has_roles isa Vector) + has_roles = [has_roles] + end + if !(match_names isa Vector) + match_names = [match_names] + end + if !(match_colors isa Vector) + match_colors = [match_colors] + end + + filtered_agents = [agent for agent in agents(world) if (length(agent_types) > 0 && typeof(agent) in agent_types) || + (length(has_roles) > 0 && _has_any_role(agent, has_roles)) || + (length(match_names) > 0 && name(agent) in match_names) || + (length(match_colors) > 0 && color(agent) in match_colors) || + (length(agent_types) == 0 && length(has_roles) == 0 && length(match_names) == 0 && length(match_colors) == 0)] + + selected_roles = [] + for agent in filtered_agents + agent_roles = roles(agent) + for role in agent_roles + if length(role_types) > 0 && typeof(role) in role_types + push!(selected_roles, (agent, role)) + end + end + end + + # found agents + roles + found_agents_caller = [[(a,a) for a in filtered_agents ]; selected_roles] + + # if only role type is provided only use the found roles + if length(agent_types) == 0 && length(role_types) > 0 + found_agents_caller = selected_roles + end + + for (agent, caller) in found_agents_caller + if !isnothing(on_message) + _add_system_handle_message_sub(agent, caller, (msg, meta) -> typeof(msg) == on_message, func, preprocessor=preprocessor) + end + if !isnothing(on_event) + _add_system_event_sub(agent, caller, on_event, (src, event) -> typeof(event) == on_event, func) + end + if !isnothing(on_global_event) + _add_system_global_event_sub(agent, caller, (event) -> typeof(event) == on_global_event, func) + end + end +end diff --git a/src/simulation/communication.jl b/src/simulation/communication.jl index 4fd971ce..e172b57a 100644 --- a/src/simulation/communication.jl +++ b/src/simulation/communication.jl @@ -1,6 +1,10 @@ -export CommunicationSimulation, PackageResult, CommunicationSimulationResult, MessagePackage, calculate_communication, SimpleCommunicationSimulation +export CommunicationSimulation, PackageResult, CommunicationSimulationResult, MessagePackage, calculate_communication, + SimpleCommunicationSimulation, DelayProviderCommunicationSimulation, create_distribution_based_com_sim using Dates +using Graphs +using MetaGraphsNext +using Distributions """ Interface to implement a communication simulation. @@ -12,7 +16,7 @@ Package result """ struct PackageResult reached::Bool - delay_s::UInt64 + delay_s::Real end """ @@ -36,7 +40,10 @@ end calculate_communication(communication_sim::CommunicationSimulation, clock::Clock, messages::Vector{MessagePackage})::CommunicationSimulationResult Calculate the communication using the specific communication simulation type. the current -simulation time `clock` and the message which shall be sent in this step `messages` +simulation time `clock` and the message which shall be sent in this step `messages`. + +Note that the method can be called multiple times with the same MessePackage objects. It is necessary that implementations of this method, will always return +the same result for the (exact!) same message package. """ function calculate_communication(communication_sim::CommunicationSimulation, clock::Clock, messages::Vector{MessagePackage})::CommunicationSimulationResult throw(ErrorException("Please implement calculate_communication(...)")) @@ -54,9 +61,6 @@ such that the delay is specified for every link between agents. delay_s_directed_edge_dict::Dict{Tuple{Union{String,Nothing},String},Real} = Dict() end -""" -Implementation for SimpleCommunicationSimulation -""" function calculate_communication(communication_sim::SimpleCommunicationSimulation, clock::Clock, messages::Vector{MessagePackage})::CommunicationSimulationResult results::Vector{PackageResult} = Vector() for message in messages @@ -69,3 +73,73 @@ function calculate_communication(communication_sim::SimpleCommunicationSimulatio end return CommunicationSimulationResult(results) end + +""" +Dynamically-based communication delay provider implementation for a communication simulation. + +With this implementation you are able to set a default provider function, which return a delay_s on +call, when no other provider are defined. To assign a speicific delay provider for an edge between +agents, `delay_s_directed_edge_dict` can be used. +""" +@kwdef struct DelayProviderCommunicationSimulation <: CommunicationSimulation + default_delay_s_provider::Function = () -> 0 + delay_s_directed_edge_dict::Dict{Tuple{Union{String,Nothing},String},Function} = Dict() + message_cache::Dict{MessagePackage,PackageResult} = Dict() +end + +function calculate_communication(communication_sim::DelayProviderCommunicationSimulation, clock::Clock, messages::Vector{MessagePackage})::CommunicationSimulationResult + results::Vector{PackageResult} = Vector() + for message in messages + if haskey(communication_sim.message_cache, message) + push!(results, communication_sim.message_cache[message]) + continue + end + key = (message.sender_id, message.receiver_id) + delay_s = communication_sim.default_delay_s_provider() + if haskey(communication_sim.delay_s_directed_edge_dict, key) + delay_s = communication_sim.delay_s_directed_edge_dict[key]() + end + pr = PackageResult(true, delay_s) + communication_sim.message_cache[message] = pr + push!(results, pr) + end + return CommunicationSimulationResult(results) +end + +function create_distribution_based_com_sim(aid_graph::MetaGraph, + agents::Vector{Agent}; + default_delay_per_edge_ms::Real=1, + base_delay_per_message_ms::Real=20, + max_edge_delay_ms::Real=100, + distribution_provider::Function=(delay) -> Poisson(delay), + label_replacer::Function=(label) -> label)::DelayProviderCommunicationSimulation + + distmatrix = fill(0, nv(aid_graph), nv(aid_graph)) + for edge in edges(aid_graph) + from = src(edge) + to = dst(edge) + distmatrix[from, to] = default_delay_per_edge_ms + distmatrix[to, from] = default_delay_per_edge_ms + end + default_distr = distribution_provider(base_delay_per_message_ms) + provider_com = DelayProviderCommunicationSimulation(default_delay_s_provider=() -> abs(rand(default_distr)) / 1000) + for agent in agents + label = aid(agent) + + label = label_replacer(label) + + code = code_for(aid_graph, label) + ds = dijkstra_shortest_paths(aid_graph, code, distmatrix) + + for (code_other, distance) in enumerate(ds.dists) + label_other = label_for(aid_graph, code_other) + if distance == typemax(Int) + distance = 100 + end + specific_distr = distribution_provider(base_delay_per_message_ms + distance) + provider_com.delay_s_directed_edge_dict[(aid(agent), label_other)] = () -> max(max_edge_delay_ms, abs(rand(specific_distr)) / 1000) + provider_com.delay_s_directed_edge_dict[(label_other, aid(agent))] = provider_com.delay_s_directed_edge_dict[(label, label_other)] + end + end + return provider_com +end \ No newline at end of file diff --git a/src/simulation/container.jl b/src/simulation/container.jl new file mode 100644 index 00000000..f4413d59 --- /dev/null +++ b/src/simulation/container.jl @@ -0,0 +1,125 @@ + + +""" +Represents a message data package including the arrival time of the package. +""" +struct MessageData + content::Any + meta::AbstractDict + arrival_time::DateTime +end + +@kwdef mutable struct SimulationContainer <: ContainerInterface + clock::Clock + env::Environment + step_size_s::Real = 0 + agents::OrderedDict{String,Agent} = OrderedDict{String,Agent}() + agent_counter::Integer = 0 + shutdown::Bool = false + message_queue::ConcurrentQueue{MessageData} = ConcurrentQueue{MessageData}() +end + +function agents(container::SimulationContainer)::Vector{Agent} + return [t[2] for t in collect(container.agents)] +end + +function messages(container::SimulationContainer)::ConcurrentQueue{MessageData} + return container.message_queue +end + +function register( + container::SimulationContainer, + agent::Agent, + suggested_aid::Union{String,Nothing}=nothing; + kwargs..., +) + actual_aid::String = "$AGENT_PREFIX$(container.agent_counter)" + if !isnothing(suggested_aid) && !haskey(container.agents, suggested_aid) + actual_aid = suggested_aid + end + container.agents[actual_aid] = agent + description(agent).aid = actual_aid + agent.context = AgentContext(container, container.env) + container.agent_counter += 1 + + return agent +end + +function forward_message(container::SimulationContainer, msg::Any, meta::AbstractDict) + push!(container.message_queue, + MessageData(msg, + meta, + add_seconds(time(container), container.step_size_s) + ) + ) + return NonWaitable() +end + +function send_message( + container::SimulationContainer, + content::Any, + agent_adress::AgentAddress, + sender_id::Union{Nothing,String}=nothing; + kwargs..., +) + receiver_id = agent_adress.aid + tracking_id = agent_adress.tracking_id + + meta = OrderedDict{String,Any}() + for (key, value) in kwargs + meta[string(key)] = value + end + + meta[RECEIVER_ID] = receiver_id + meta[SENDER_ID] = sender_id + meta[TRACKING_ID] = tracking_id + meta[SENDER_ADDR] = nothing + + @debug "Send a message to ($receiver_id), from $sender_id" typeof(content) + + return forward_message(container, content, meta) +end + + +function process_message(container::SimulationContainer, msg::Any, meta::AbstractDict) + receiver_id = meta[RECEIVER_ID] + + if !haskey(container.agents, meta[RECEIVER_ID]) + @warn "The container has no agent with id: $receiver_id (from $(sender_address(meta)) with $(typeof(msg)))" keys(container.agents) msg meta + else + agent = container.agents[receiver_id] + return dispatch_message(agent, msg, meta) + end +end + +""" + Base.getindex(container::SimulationContainer, index::String) + +Return the agent indexed by `index` (aid). +""" +function Base.getindex(container::SimulationContainer, index::String) + return container.agents[index] +end +function Base.getindex(container::SimulationContainer, index::Int) + return agents(container)[index] +end + +function shutdown(container::SimulationContainer) + container.shutdown = true + + for agent in agents(container) + shutdown(agent) + end +end + +function protocol_addr(container::SimulationContainer) + return nothing +end + +function clock(container::SimulationContainer) + return container.clock +end + +function time(container::SimulationContainer) + return clock(container).simulation_time +end \ No newline at end of file diff --git a/src/simulation/tasks.jl b/src/simulation/tasks.jl index 434f6308..d2329372 100644 --- a/src/simulation/tasks.jl +++ b/src/simulation/tasks.jl @@ -57,123 +57,20 @@ function determine_next_event_time(task_sim::TaskSimulation) throw(ErrorException("Please implement determine_next_event_time(...)")) end -""" -Specific scheduler, defined to be injected to the agents and intercept scheduling -calls and especially the sleep calls while scheduling. This struct manages all necessary times and -events, which shall fulfill the purpose to step the tasks only for a given step_size. -""" -@kwdef struct SimulationScheduler <: AbstractScheduler - clock::Clock - events::ConcurrentDict{Task,Tuple{Base.Event,DateTime}} = ConcurrentDict{Task,Tuple{Base.Event,DateTime}}() - tasks::ConcurrentDict{Task,Tuple{TaskData,Base.Event}} = ConcurrentDict{Task,Tuple{TaskData,Base.Event}}() - queue::ConcurrentQueue{Union{Tuple{Function,TaskData,Base.Event},Task}} = ConcurrentQueue{Union{Tuple{Function,TaskData,Base.Event},Task}}() - wait_queue::ConcurrentQueue{Task} = ConcurrentQueue{Task}() -end - -""" -Internal struct, signaling the state of the tasks which has been waited on. -""" -struct WaitResult - cont::Bool - result::Any -end - -function determine_next_event_time_with(scheduler::SimulationScheduler, simulation_time::DateTime) - lowest = nothing - - # normal queue - next = scheduler.queue.head.next - while !isnothing(next) - if isa(next.value, Tuple) - return 0 - else - throw("This should not happen! Did you schedule a task with zero sleep time?") - end - next = next.next - end - - # wait queue - next = scheduler.wait_queue.head.next - while !isnothing(next) - t = scheduler.events[next.value][2] - if isnothing(lowest) || t < lowest - lowest = t - end - next = next.next - end - if isnothing(lowest) - return nothing - end - return (lowest - simulation_time).value / 1000 -end - -function wait_for_finish_or_sleeping(scheduler::SimulationScheduler, task::Task, step_size_s::Real, timeout_s::Real=10, check_delay_s=0.001)::WaitResult - remaining = timeout_s - while remaining > 0 - sleep(check_delay_s) - remaining -= check_delay_s - if !istaskdone(task) - if haskey(scheduler.events, task) - event_time = scheduler.events[task] - @debug "not done, found event" event_time[2] add_seconds(scheduler.clock.simulation_time, step_size_s) - if event_time[2] <= add_seconds(scheduler.clock.simulation_time, step_size_s) - return WaitResult(true, nothing) - else - return WaitResult(false, nothing) - end - end - else - return WaitResult(false, Some(task.result)) - end - end - throw("Simulation encountered a task timeout!") -end - -function now(scheduler::SimulationScheduler) - return scheduler.clock.simulation_time -end - -function sleep(scheduler::SimulationScheduler, time_s::Real) - event = Base.Event() - ctime = scheduler.clock.simulation_time - if haskey(scheduler.events, current_task()) - ctime = scheduler.events[current_task()][2] - end - scheduler.events[current_task()] = (event, add_seconds(ctime, time_s)) - @debug "Sleep task with" current_task() event add_seconds(ctime, time_s) - wait(event) -end - -function wait(scheduler::SimulationScheduler, timer::Timer, delay_s::Real) - sleep(scheduler, delay_s) -end - -function tasks(scheduler::SimulationScheduler) - return scheduler.tasks -end - -function schedule(f::Function, scheduler::SimulationScheduler, data::TaskData) - event = Base.Event() - push!(scheduler.queue, (f, data, event)) - return event -end - -function do_schedule(f::Function, scheduler::SimulationScheduler, data::TaskData, event::Base.Event) - task = Threads.@spawn execute_task(f, scheduler, data) - tasks(scheduler)[task] = (data, event) - return task -end - """ Default implementation of the interface. """ @kwdef mutable struct SimpleTaskSimulation <: TaskSimulation clock::Clock - agent_schedulers::Vector{SimulationScheduler} = Vector{SimulationScheduler}() + simulation_schedulers::Vector{SimulationScheduler} = Vector{SimulationScheduler}() +end + +function add_simulation_scheduler!(task_sim::SimpleTaskSimulation, simulation_scheduler::SimulationScheduler) + push!(task_sim.simulation_schedulers, simulation_scheduler) end function determine_next_event_time(task_sim::SimpleTaskSimulation) - event_times = [determine_next_event_time_with(scheduler, task_sim.clock.simulation_time) for scheduler in task_sim.agent_schedulers] + event_times = [determine_next_event_time_with(scheduler, task_sim.clock.simulation_time) for scheduler in task_sim.simulation_schedulers] event_times = event_times[event_times.!=nothing] if length(event_times) <= 0 return nothing @@ -183,7 +80,7 @@ end function create_agent_scheduler(task_sim::SimpleTaskSimulation) scheduler = SimulationScheduler(clock=task_sim.clock) - push!(task_sim.agent_schedulers, scheduler) + push!(task_sim.simulation_schedulers, scheduler) return scheduler end @@ -197,64 +94,91 @@ function transfer_wait_queue(scheduler::SimulationScheduler) end end +function execute_task_for(task_sim::SimpleTaskSimulation, + scheduler::SimulationScheduler, + result::TaskIterationResult, + step_size_s::Real) + + while true + next_task = maybepopfirst!(scheduler.queue) + if isnothing(next_task) + break + end + + # Every time a task is running the state can change, so another iteration has to be calculated + result.state_changed = true + + task = something(next_task) + if task isa Task + @debug "Continue the old Task!" task + + # in this state the task need to wait on an event + event = scheduler.events[task] + event_time = scheduler.task_time[task] + # only continue if the event time has been reached + if event_time <= add_seconds(scheduler.clock.simulation_time, step_size_s) + @debug "Notify!" task event_time add_seconds(scheduler.clock.simulation_time, step_size_s) + maybepop!(scheduler.events, task) + notify(event) + else + push!(scheduler.wait_queue, task) + @debug "Skip old Task!" task + continue + end + else + @debug "Processing new Task!" + func, td, event = task + task = do_schedule(func, scheduler, td, event) + end + + @debug "Waiting..." + out = wait_for_finish_or_sleeping(scheduler, task, step_size_s) + @debug "Finished..." + + if !isnothing(out.result) + notify(scheduler.tasks[task][2]) + result.task_to_result[uuid4()] = TaskResult(true, task_sim.clock.simulation_time, out) + + # clean up task data + maybepop!(scheduler.tasks, task) + + # rethrow exception if exists + if istaskfailed(task) + log_exception(task.exception, task.backtrace) + throw(task.exception) + end + + @debug "A task has been finished" out.result + elseif out.cont + push!(scheduler.queue, task) + @debug "The task $task needs another iteration!" + else + push!(scheduler.wait_queue, task) + @debug "The task will be proccesed in the next step_iteration" + end + end +end + function step_iteration(task_sim::SimpleTaskSimulation, step_size_s::Real, first_step=false)::TaskIterationResult # Transfer Tasks from the previous iteration which are still running # Only if, this was the last iteration of a step if first_step - for scheduler in task_sim.agent_schedulers + for scheduler in task_sim.simulation_schedulers transfer_wait_queue(scheduler) end end result = TaskIterationResult() @sync begin - for scheduler in task_sim.agent_schedulers + for scheduler in task_sim.simulation_schedulers # Execute all tasks subsequently until no task can or is allowed to run # based on the simulation time - Threads.@spawn begin - while true - next_task = maybepopfirst!(scheduler.queue) - if isnothing(next_task) - break - end - - # Every time a task is running the state can change, so another iteration has to be calced - result.state_changed = true - - task = something(next_task) - if isa(task, Task) - @debug "Continue the old Task!" task - notify(scheduler.events[task][1]) - else - @debug "Processing new Task!" - func, td, event = task - task = do_schedule(func, scheduler, td, event) - end - - @debug "Waiting..." - out = wait_for_finish_or_sleeping(scheduler, task, step_size_s) - @debug "Finished..." - - if !isnothing(out.result) - notify(scheduler.tasks[task][2]) - result.task_to_result[uuid4()] = TaskResult(true, task_sim.clock.simulation_time, out) - - # clean up task data - maybepop!(scheduler.tasks, task) - if haskey(scheduler.events, task) - maybepop!(scheduler.events, task) - end - - @debug "A task has been finished" out.result - elseif out.cont - push!(scheduler.queue, task) - @debug "The task $task needs another iteration!" - else - push!(scheduler.wait_queue, task) - @debug "The task will be proccesed in the next step_iteration" - end - end + Threads.@spawn try + execute_task_for(task_sim, scheduler, result, step_size_s) + catch ex + log_exception(ex) + rethrow(ex) end end end diff --git a/src/simulation/world.jl b/src/simulation/world.jl new file mode 100644 index 00000000..8deb0019 --- /dev/null +++ b/src/simulation/world.jl @@ -0,0 +1,590 @@ +export World, register, send_message, shutdown, protocol_addr, + create_world, step_simulation, SimulationResult, CommunicationSimulationResult, + TaskSimulationResult, on_step, discrete_step_until, env, space, time, clock, + record_world!, record_agent!, record_agent_having!, MessageTransaction, data_collection, data_agent_collection + +using Base.Threads +using Dates +using ConcurrentCollections +using OrderedCollections + +""" +Id key for the receiver in the meta dict +""" +RECEIVER_ID::String = "receiver_id" +""" +Prefix for the generated aid's +""" +AGENT_PREFIX::String = "agent" +""" +DISCRETE EVENT STEP SIZE +""" +DISCRETE_EVENT::Real = -1 + +""" + create_world(start_time::DateTime; communication_sim::Union{Nothing,CommunicationSimulation}=nothing, task_sim::Union{Nothing,TaskSimulation}=nothing) + +Create a simulation world. The world is intitialized with `start_time`. + +Per default the [`SimpleCommunicationSimulation`](@ref) is used for communication simulation, and +[`SimpleTaskSimulation`](@ref) for simulating the tasks of agents. To replace these, `communication_sim` +and respectively `task_sim` can be set. +""" +function create_world(start_time::DateTime; + communication_sim::Union{Nothing,CommunicationSimulation}=nothing, + task_sim::Union{Nothing,TaskSimulation}=nothing, + space::Union{Nothing,Space}=nothing, + behavior::Union{Nothing,Behavior}=nothing) + + world = World(clock=Clock(start_time)) + if !isnothing(communication_sim) + world.communication_sim = communication_sim + end + if !isnothing(task_sim) + world.task_sim = task_sim + end + if !isnothing(space) + world.env.space = space + end + if !isnothing(behavior) + world.env.behavior = behavior + end + add_observer!(world.env, world.world_observer) + add_simulation_scheduler!(world.task_sim, world.env.scheduler) + return world +end + +struct DispatchToAgentWorldObserver <: WorldObserver + agents_ref::OrderedDict{String,Agent} +end + +function dispatch_global_event(observer::DispatchToAgentWorldObserver, event::Any) + for agent in values(observer.agents_ref) + dispatch_global_event(agent, event) + end +end + +""" +A WorldRecording is a container to record data in the world. +""" +@kwdef mutable struct WorldRecording + timeseries::Vector{Any} = Vector() + time::Vector{Real} = Vector() + data::Any = nothing + no_plot = false +end + +""" +An AgentsRecording is a container to record data of the agents. +""" +@kwdef mutable struct AgentsRecording + timeseries::Dict{String,Vector{Any}} = Dict() + time::Vector{Real} = Vector() + data::Any = nothing + dedicated_plots = false + no_plot = false +end + +struct MessageTransaction + sender_id::Union{String,Nothing} + receiver_id::String + sent_date::DateTime + arriving_date::DateTime + content::Any +end + +""" +The World used as a base struct to enable simulations in Mango.jl. Always create using [`create_world`](@ref). +""" +@kwdef mutable struct World <: ContainerInterface + clock::Clock = Clock(DateTime(0)) + env::Environment = DefaultEnvironment(scheduler=SimulationScheduler(clock=clock)) + container::SimulationContainer = SimulationContainer(clock=clock, env=env) + task_sim::TaskSimulation = SimpleTaskSimulation(clock=clock) + communication_sim::CommunicationSimulation = SimpleCommunicationSimulation() + world_observer::WorldObserver = DispatchToAgentWorldObserver(container.agents) + data_collections::OrderedDict{String,WorldRecording} = OrderedDict() + data_agent_collections::OrderedDict{String,AgentsRecording} = OrderedDict() + data_collectors::Vector{Function} = Vector() + recorded_messages::Vector{MessageTransaction} = Vector() +end + +function agents(world::World)::Vector{Agent} + return agents(world.container) +end + +""" + on_step(agent::Agent, env::Environment, clock::Clock, step_size_s::Real) + +Hook-in, called on every step of the simulation world for every `agent`. + +Further, the `world` is passed, which represents a common view on the environment +in which agents can interact with eachother. Besides, the `clock` and the `step_size_s` +can be used to read the current simulation time and the time which passes in the current step. +""" +function on_step(agent::Agent, env::Environment, clock::Clock, step_size_s::Real) + # default nothing +end + +function on_step(role::Role, env::Environment, clock::Clock, step_size_s::Real) + # default nothing +end + +""" +Internal, call on_step on all agents. +""" +function step_agent(agent::Agent, env::Environment, clock::Clock, step_size_s::Real) + on_step(agent, env, clock, step_size_s) + for role in roles(agent) + on_step(role, env, clock, step_size_s) + end +end + +""" +Contains the result of the communication simulation and whether the state of +the world has changed +""" +struct MessagingIterationResult + communication_result::CommunicationSimulationResult + state_changed::Bool +end + +""" +Result of all messaging simulation iterations. +""" +@kwdef struct MessagingSimulationResult + results::Vector{MessagingIterationResult} = Vector() +end + +""" +Result of all task simulation iterations. +""" +@kwdef struct TaskSimulationResult + results::Vector{TaskIterationResult} = Vector() +end + +""" +Result of one simulation step. +""" +struct SimulationResult + time_elapsed::Real + messaging_result::MessagingSimulationResult + task_result::TaskSimulationResult + simulation_step_size_s::Real +end + +""" +Internal +""" +function to_message_package(message_data::MessageData)::MessagePackage + sender_aid = message_data.meta[SENDER_ID] + receiver_aid = message_data.meta[RECEIVER_ID] + return MessagePackage(sender_aid, receiver_aid, message_data.arrival_time, (message_data.content, message_data.meta)) +end + +""" +Internal +""" +function to_cs_input!(message_queue::ConcurrentQueue{MessageData})::Vector{MessagePackage} + messages_packages = Vector() + while true + some_message = maybepopfirst!(message_queue) + if isnothing(some_message) + break + end + message::MessageData = something(some_message) + push!(messages_packages, to_message_package(message)) + end + return messages_packages +end + +""" +Internal +""" +function to_cs_input(message_queue::ConcurrentQueue{MessageData})::Vector{MessagePackage} + messages_packages = Vector() + next = message_queue.head.next + while !isnothing(next) + message::MessageData = next.value + push!(messages_packages, to_message_package(message)) + next = next.next + end + return messages_packages +end + +""" +Internal +""" +function cs_step_iteration(world::World, + step_size_s::Real, + pre_communication_result::Union{Nothing,CommunicationSimulationResult})::MessagingIterationResult + message_packages = to_cs_input!(messages(world.container)) + communication_result = pre_communication_result + if isnothing(communication_result) + communication_result = calculate_communication(world.communication_sim, + clock(world), + message_packages) + end + state_changed = false + @sync begin + for (mp, pr) in sort([z for z in zip(message_packages, communication_result.package_results)], by=t -> add_seconds(t[1].sent_date, t[2].delay_s)) + if add_seconds(mp.sent_date, pr.delay_s) <= add_seconds(time(world), step_size_s) && pr.reached + state_changed = true + push!(world.recorded_messages, MessageTransaction(mp.sender_id, + mp.receiver_id, + mp.sent_date, + add_seconds(mp.sent_date, pr.delay_s), + mp.content[1])) + Threads.@spawn try + process_message(world.container, mp.content[1], mp.content[2]) + catch ex + log_exception(ex) + rethrow(ex) + end + else + # process it later + push!(messages(world.container), MessageData(mp.content[1], mp.content[2], mp.sent_date)) + end + end + end + return MessagingIterationResult(communication_result, state_changed) +end + +""" +Internal +""" +function determine_time_step(world::World) + message_packages = to_cs_input(messages(world.container)) + communication_result = calculate_communication(world.communication_sim, clock(world), message_packages) + + # earliest message or -1 if no message arrives + message_arrival_times = [add_seconds(t[1].sent_date, t[2].delay_s) for t in zip(message_packages, communication_result.package_results)] + time_to_next_message_s = nothing + if length(message_arrival_times) > 0 + time_to_next_message_s = (findmin(message_arrival_times)[1] - time(world)).value / 1000 + end + @debug "Next message in $time_to_next_message_s" + + # ealiest task or -1 if no task scheduled + next_event_s = determine_next_event_time(world.task_sim) + + @debug "Next event in $next_event_s" + + # check whether one is absent and the other is present + if isnothing(time_to_next_message_s) && isnothing(next_event_s) + return nothing, communication_result + elseif isnothing(next_event_s) + return time_to_next_message_s, communication_result + elseif isnothing(time_to_next_message_s) + return next_event_s, communication_result + end + + # return earliest + return min(time_to_next_message_s, next_event_s), communication_result +end + +""" + record!(recording::WorldRecording, time::Real, data::Any) + +Record data `data` at time `time` in the `recording`. +""" +function insert_world_recording!(recording::WorldRecording, world::World, data::Any) + push!(recording.time, seconds_elapsed(clock(world))) + push!(recording.timeseries, data) +end + +function insert_agent_recording!(recording::AgentsRecording, world::World, agent::Agent, data::Any) + timeseries = get!(recording.timeseries, aid(agent), Vector()) + push!(timeseries, data) +end + +function do_recordings(world::World) + for collector in world.data_collectors + collector(world) + end +end + +function step_all_entities(world::World, time_step_s::Real) + + # Stepping of the hook-based entities always happens + step(world.env, clock(world), time_step_s) + # agents act on the stepping hook + for agent in values(agents(world)) + step_agent(agent, world.env, clock(world), time_step_s) + end +end + +elapsed_det::Real = 0 +elapsed_step::Real = 0 +elapsed_sim::Real = 0 +elapsed_rec::Real = 0 + +""" + step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT; max_advance_time_s::Real=-1)::Union{SimulationResult,Nothing} + +Step the simulation using a continous time-span or until the next event happens. + +For the continous simulation a `step_size_s` can be freely chosen, for the discrete event type +DISCRETE_EVENT has to be set for the `step_size_s`. If you choose DISCRETE_EVENT, you can also specify +a max_advance_time_s, which will abort the step if the determined step_size exceeds the max_advance_time_s. +""" +function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT; max_advance_time_s::Real=-1)::Union{SimulationResult,Nothing} + # Init world if uninitialized + if !initialized(world.env) + initialize(world.env, [v for v in values(agents(world))]) + do_recordings(world) + end + + state_changed = true + + @debug "Time at the start of the step" time(world) + + task_sim_result = TaskSimulationResult() + messaging_sim_result = MessagingSimulationResult() + first_step = true + time_step_s = step_size_s + + elapsed = @elapsed begin + # We are in discrete event mode, so we need to determine + # the time until the next event occurs, this time will + # be used to execute the time-based simulation + comm_result = nothing + if time_step_s == DISCRETE_EVENT + time_step_s, comm_result = determine_time_step(world) + @debug "Determined the size to be $time_step_s" + if isnothing(time_step_s) || (max_advance_time_s != -1 && time_step_s > max_advance_time_s) + # only step guaranteed entities + step_all_entities(world, 0) + return nothing + end + end + world.container.step_size_s = time_step_s + end + + global elapsed_det + elapsed_det += elapsed + @debug "The determine step needed $elapsed seconds" + + elapsed = @elapsed begin + step_all_entities(world, time_step_s) + end + + global elapsed_step + elapsed_step += elapsed + @debug "The steps enti step needed $elapsed seconds" + + elapsed = @elapsed begin + # now we process everything which happened in the steps, + # tasks and previous iterations + while state_changed + @debug "Start simulation iteration" + task_iter_result = nothing + comm_iter_result = nothing + @sync begin + + Threads.@spawn try + comm_iter_result = cs_step_iteration(world, time_step_s, first_step ? comm_result : nothing) + catch ex + log_exception(ex) + rethrow(ex) + end + + Threads.@spawn try + task_iter_result = step_iteration(world.task_sim, time_step_s, first_step) + catch ex + log_exception(ex) + rethrow(ex) + end + end + first_step = false + push!(task_sim_result.results, task_iter_result) + push!(messaging_sim_result.results, comm_iter_result) + state_changed = comm_iter_result.state_changed || task_iter_result.state_changed + @debug "Finish simulation iteration" state_changed + end + end + + global elapsed_sim + elapsed_sim += elapsed + @debug "The simulation step needed $elapsed seconds" + + elapsed = @elapsed begin + world.clock.simulation_time = add_seconds(time(world), time_step_s) + world.container.step_size_s = 0 + + @debug "New time" time(world) + + do_recordings(world) + end + + global elapsed_rec + elapsed_rec += elapsed + @debug "The recording update step needed $elapsed seconds" + + return SimulationResult(elapsed, messaging_sim_result, task_sim_result, time_step_s) +end + +""" + discrete_event_simulation(world::World, max_advance_time_s::Real) + +Execute a discrete event simulation using the `world` with the maximal allowed advanced time +of the simulation of `max_advance_time_s`. + +This function will step the world until the clock has advanced to the initial_time + `max_advance_time_s` +or if the time of the world does not advance anymore (which would mean no events are scheduled). +""" +function discrete_step_until(world::World, max_advance_time_s::Real) + global elapsed_det, elapsed_step, elapsed_sim, elapsed_rec + elapsed_det = elapsed_rec = elapsed_sim = elapsed_step = 0 + + initial_time = time(world) + prev_time = nothing + results = [] + max_time = add_seconds(initial_time, max_advance_time_s) + + elapsed = @elapsed begin + while isnothing(prev_time) || ((prev_time < time(world) || length(results) == 1) + && + max_time > time(world)) + + prev_time = time(world) + push!(results, step_simulation(world, max_advance_time_s=(max_time - prev_time).value / 1000)) + end + end + @info "The discrete event simulation needed $elapsed seconds" + @info "The different parts needed" elapsed_det elapsed_step elapsed_sim elapsed_rec + + return results +end + +struct NonWaitable end +function Base.wait(waitable::NonWaitable) end + +function env(world::World) + return world.env +end + +function space(world::World) + return space(world.env) +end + +function clock(world::World) + return world.clock +end + +function time(world::World) + return clock(world).simulation_time +end + +function register( + world::World, + agent::Agent, + suggested_aid::Union{String,Nothing}=nothing; + kwargs..., +) + if !isnothing(world.task_sim) + agent.scheduler = create_agent_scheduler(world.task_sim) + end + agent = register(world.container, agent, suggested_aid, kwargs...) + return agent +end + +""" + data_collection(world::World, key::String) + +Return the data collection with the `key` from the world. +""" +function data_collection(world::World, key::String; no_plot::Bool=false) + return get!(world.data_collections, key, WorldRecording(no_plot=no_plot)) +end + +""" + data_agent_collection(world::World, key::String) + +Return the data collection with the `key` from the world. +""" +function data_agent_collection(world::World, key::String; dedicated_plots::Bool=false, no_plot::Bool=false) + return get!(world.data_agent_collections, key, AgentsRecording(dedicated_plots=dedicated_plots, no_plot=no_plot)) +end + +""" + collect_data(collector::Function, world::World, key::String) + +Collect data from the world using the `collector` function and +store it in the data collection with the `key`. +""" +function collect_data(collector::Function, world::World, key::String; no_plot::Bool=false) + push!(world.data_collectors, (world) -> collector(world, data_collection(world, key, no_plot=no_plot))) +end + +""" + collect_agent_data(collector::Function, world::World, key::String) + +Collect data from the agents in the world using the `collector` function and +store it in the data collection with the `key`. + +The data can be plotted using plot_agents. +""" +function collect_agent_data(collector::Function, world::World, key::String; dedicated_plots::Bool=false, no_plot::Bool=false) + dac = data_agent_collection(world, key, dedicated_plots=dedicated_plots, no_plot=no_plot) + for agent in values(agents(world)) + push!(world.data_collectors, (world) -> collector(world, agent, dac)) + end + push!(world.data_collectors, (world) -> push!(dac.time, seconds_elapsed(clock(world)))) +end + +""" + record_world!(world_recorder::Function, world::World, key::String) + +Record the world using the `world_recorder` function and store it in the data collection with the `key`. + +The data can be plotted using plot_world. +""" +function record_world!(world_recorder::Function, world::World, key::String; no_plot::Bool=false) + collect_data(world, key, no_plot=no_plot) do w, dc + insert_world_recording!(dc, w, world_recorder()) + end +end + +""" + record_agent!(agent_recorder::Function, world::World, key::String) + +Record the agents in the world using the `agent_recorder` function and store +it in the data collection with the `key`. The data can be plotted using plot_agents. + +""" +function record_agent!(agent_recorder::Function, world::World, key::String; dedicated_plots::Bool=false, no_plot::Bool=false) + collect_agent_data(world, key, dedicated_plots=dedicated_plots, no_plot=no_plot) do w, a, dc + insert_agent_recording!(dc, w, a, agent_recorder(a)) + end +end + +function record_agent_having!(agent_recorder::Function, + world::World, + key::String, + role_type::DataType; + agent_color::Union{Nothing,Symbol}=nothing, + aid_contains::Union{Nothing,String}=nothing, + dedicated_plots::Bool=false) + + collect_agent_data(world, key, dedicated_plots=dedicated_plots) do w, a, dc + if has_role(a, role_type) && + (isnothing(agent_color) || agent_color == color(a)) && + (isnothing(aid_contains) || occursin(aid_contains, aid(a))) + + insert_agent_recording!(dc, w, a, agent_recorder(a)) + end + end +end + +function Base.getindex(world::World, index::String) + return world.container[index] +end + +function Base.getindex(world::World, index::Int) + return world.container[index] +end + +function shutdown(world::World) + shutdown(world.container) +end diff --git a/src/util/error_handling.jl b/src/util/error_handling.jl index 7c0388bf..2e59643c 100644 --- a/src/util/error_handling.jl +++ b/src/util/error_handling.jl @@ -1,11 +1,24 @@ +function log_exception(e, backtrace=nothing) + bt = catch_backtrace() + if !isnothing(backtrace) + bt = backtrace + end + msg = sprint(io -> begin + println(io, "Exception occurred in thread ", Threads.threadid()) + showerror(io, e) + println(io) + Base.show_backtrace(io, bt) + end) + @error msg +end + macro spawnlog(expr) quote Threads.@spawn try $(esc(expr)) catch ex - bt = stacktrace(catch_backtrace()) - showerror(stderr, ex, bt) + log_exception(ex) rethrow(ex) end end diff --git a/src/util/scheduling.jl b/src/util/scheduling.jl index 35ad44d0..0d6521a4 100644 --- a/src/util/scheduling.jl +++ b/src/util/scheduling.jl @@ -1,6 +1,7 @@ export TaskData, PeriodicTaskData, InstantTaskData, + DelayTaskData, DateTimeTaskData, AwaitableTaskData, ConditionalTaskData, @@ -10,26 +11,42 @@ export TaskData, stop_and_wait_for_all_tasks, schedule, Clock, + DateTimeClock, Scheduler, SimulationScheduler, AbstractScheduler, - sleep_until + AbstractClock, + sleep_until, + seconds_elapsed, + tasks, + clock using Dates using ConcurrentCollections -import Base.schedule, Base.sleep, Base.wait +import Base.schedule, Base.sleep, Base.wait, Base.notify """ Abstract type of a clock, which holds the time of a simulation """ abstract type AbstractClock end +function time(clock::AbstractClock) + throw("Not defined!") +end +function seconds_elapsed(clock::AbstractClock) + throw("Not defined!") +end + """ Default clock implementation, in which a static DateTime field is used. """ @kwdef mutable struct Clock <: AbstractClock simulation_time::DateTime + initial_time::DateTime + function Clock(simulation_time) + return new(simulation_time, simulation_time) + end end """ @@ -38,6 +55,13 @@ Clock implmentation using the real time and therefore not holding any time infor struct DateTimeClock <: AbstractClock end +function time(clock::Clock) + return clock.simulation_time +end +function seconds_elapsed(clock::Clock) + return (time(clock) - clock.initial_time).value / 1000 +end + struct Stop end struct Continue end @@ -68,7 +92,7 @@ abstract type AbstractScheduler end Internal, return the time on which the scheduler is working on """ function now(scheduler::AbstractScheduler) - return DateTime.now() + return Dates.now() end """ @@ -90,13 +114,27 @@ function wait(scheduler::AbstractScheduler, timer::Timer, delay_s::Real) return wait(timer) end + +""" + wait(scheduler::AbstractScheduler, awaitable::Any) + +Wait on awaitable based on its schedulers policy. +""" +function wait(scheduler::AbstractScheduler, awaitable::Any) + return wait(awaitable) +end + +function notify(scheduler::AbstractScheduler, event::Threads.Event) + return notify(event) +end + """ clock(scheduler::AbstractScheduler) Return the internal time representation, the `clock`. """ function clock(scheduler::AbstractScheduler)::AbstractClock - throw("unimplemented") + throw(InvalidStateException("unimplemented", :NotImplemented)) end """ tasks(scheduler::AbstractScheduler) @@ -104,7 +142,7 @@ end Return the tasks currently on schedule and managed by the scheduler. """ function tasks(scheduler::AbstractScheduler) - throw("unimplemented") + throw(InvalidStateException("unimplemented", :NotImplemented)) end """ @@ -159,6 +197,13 @@ Instant task data. Functions scheduled with this data is scheduled instantly. """ struct InstantTaskData <: TaskData end +""" +Delayed task data. Functions scheduleld with this data are delays by delay_s seconds +""" +struct DelayTaskData <: TaskData + delay_s::Real +end + """ Schedule the function at a specific time determined by the date::DateTime. """ @@ -174,6 +219,15 @@ struct AwaitableTaskData <: TaskData awaitable::Any end +""" + wait(scheduler::AbstractScheduler, awaitable_task_data::AwaitableTaskData) + +Wait for the awaitable data in `awaitable_task_data`. +""" +function wait(scheduler::AbstractScheduler, awaitable_task_data::AwaitableTaskData) + return wait(awaitable_task_data.awaitable) +end + """ Schedule the function when the `condition` is fulfilled. To check whether it is fulfilled the condition function is called every `check_interval_s`. @@ -194,13 +248,18 @@ function execute_task(f::Function, scheduler::AbstractScheduler, data::InstantTa f() end +function execute_task(f::Function, scheduler::AbstractScheduler, data::DelayTaskData) + sleep(scheduler, data.delay_s) + f() +end + function execute_task(f::Function, scheduler::AbstractScheduler, data::DateTimeTaskData) - sleep(scheduler, (data.date - Dates.now()).value / 1000) + sleep(scheduler, (data.date - now(scheduler)).value / 1000) f() end function execute_task(f::Function, scheduler::AbstractScheduler, data::AwaitableTaskData) - wait(data.awaitable) + wait(scheduler, data) f() end @@ -219,7 +278,7 @@ functino `f` is scheduled using the information in `data`, which specifies the w scheduled. """ function schedule(f::Function, scheduler::AbstractScheduler, data::TaskData) - task = Threads.@spawn execute_task(f, scheduler, data) + task = @spawnlog execute_task(f, scheduler, data) tasks(scheduler)[task] = data return task end @@ -295,4 +354,143 @@ function sleep_until(condition::Function; interval_s::Real=0.01) while !condition() sleep(interval_s) end +end + +### Simulation Scheduler + + +""" +Specific scheduler, defined to be injected to the agents and intercept scheduling +calls and especially the sleep calls while scheduling. This struct manages all necessary times and +events fulfilling the purpose to step the tasks only for a given step_size. +""" +@kwdef struct SimulationScheduler <: AbstractScheduler + clock::Clock + events::ConcurrentDict{Task,Base.Event} = ConcurrentDict{Task,Base.Event}() + task_time::ConcurrentDict{Task,DateTime} = ConcurrentDict{Task,DateTime}() + tasks::ConcurrentDict{Task,Tuple{TaskData,Base.Event}} = ConcurrentDict{Task,Tuple{TaskData,Base.Event}}() + queue::ConcurrentQueue{Union{Tuple{Function,TaskData,Base.Event},Task}} = ConcurrentQueue{Union{Tuple{Function,TaskData,Base.Event},Task}}() + wait_queue::ConcurrentQueue{Task} = ConcurrentQueue{Task}() +end + +""" +Internal struct, signaling the state of the tasks which has been waited on. +""" +struct WaitResult + cont::Bool + result::Any +end + +function stop_and_wait_for_all_tasks(scheduler::SimulationScheduler) + # do nothing, as task simulation will handle that part. +end + +function determine_next_event_time_with(scheduler::SimulationScheduler, simulation_time::DateTime) + lowest = nothing + + # normal queue + next = scheduler.queue.head.next + while !isnothing(next) + if isa(next.value, Tuple) + return 0 + else + throw("This should not happen! Did you schedule a task with zero sleep time?") + end + next = next.next + end + + # wait queue + next = scheduler.wait_queue.head.next + while !isnothing(next) + t = scheduler.task_time[next.value] + if isnothing(lowest) || t < lowest + lowest = t + end + next = next.next + end + if isnothing(lowest) + return nothing + end + return (lowest - simulation_time).value / 1000 +end + +function wait_for_finish_or_sleeping(scheduler::SimulationScheduler, task::Task, step_size_s::Real, timeout_s::Real=10, check_delay_s=0.001)::WaitResult + remaining = timeout_s + while remaining > 0 + if !istaskdone(task) + if haskey(scheduler.events, task) + event_time = scheduler.task_time[task] + @debug "not done, found event" event_time add_seconds(scheduler.clock.simulation_time, step_size_s) + if event_time <= add_seconds(scheduler.clock.simulation_time, step_size_s) + return WaitResult(true, nothing) + else + return WaitResult(false, nothing) + end + end + else + return WaitResult(false, Some(task.result)) + end + sleep(check_delay_s) + remaining -= check_delay_s + end + throw("Simulation encountered a task timeout!") +end + +function now(scheduler::SimulationScheduler) + return scheduler.clock.simulation_time +end + +function sleep(scheduler::SimulationScheduler, time_s::Real) + event = Base.Event() + ctime = scheduler.clock.simulation_time + if haskey(scheduler.task_time, current_task()) + ctime = scheduler.task_time[current_task()] + end + scheduler.events[current_task()] = event + scheduler.task_time[current_task()] = add_seconds(ctime, time_s) + @debug "Sleep task with" current_task() event ctime time_s + wait(event) +end + +function wait(scheduler::SimulationScheduler, timer::Timer, delay_s::Real) + sleep(scheduler, delay_s) +end + +function wait(scheduler::SimulationScheduler, awaitable_task_data::AwaitableTaskData) + elapsed = @elapsed wait(awaitable_task_data.awaitable) + sleep(scheduler, elapsed) +end + +function wait(scheduler::SimulationScheduler, event::Threads.Event) + ctime = scheduler.clock.simulation_time + if haskey(scheduler.task_time, current_task()) + ctime = scheduler.task_time[current_task()] + end + scheduler.events[current_task()] = event + scheduler.task_time[current_task()] = DateTime(9999) + return wait(event) +end + +function notify(scheduler::SimulationScheduler, event::Threads.Event) + maybepop!(scheduler.events, current_task()) + scheduler.task_time[current_task()] = DateTime(0) + return Base.notify(event) +end + +function tasks(scheduler::SimulationScheduler) + return scheduler.tasks +end + +clock(scheduler::SimulationScheduler) = scheduler.clock + +function schedule(f::Function, scheduler::SimulationScheduler, data::TaskData) + event = Base.Event() + push!(scheduler.queue, (f, data, event)) + return event +end + +function do_schedule(f::Function, scheduler::SimulationScheduler, data::TaskData, event::Base.Event) + task = Threads.@spawn execute_task(f, scheduler, data) + tasks(scheduler)[task] = (data, event) + return task end \ No newline at end of file diff --git a/src/util/topology.jl b/src/util/topology.jl new file mode 100644 index 00000000..0d6d9ff9 --- /dev/null +++ b/src/util/topology.jl @@ -0,0 +1,597 @@ +export complete_topology, star_topology, cycle_topology, graph_topology, per_node, add!, + topology_neighbors, create_topology, add_node!, add_edge!, Topology, modify_topology, + choose_agents!, assign_agents!, NORMAL, BROKEN, INACTIVE, set_edge_state!, remove_edge!, remove_node!, + auto_assign!, topology_node_id, topology_to_aid_graph, set_as_connector!, connect_topologies!, mark_as_connector!, + topology_connectors, topology_connection_types, NORMAL, INACTIVE, BROKEN, UNKNOWN, EXT_CONNECTION, State, topology_service, + set_characteristic!, topology_characteristic + +using MetaGraphsNext +using Graphs +import Graphs.add_edge! + +@kwdef struct Node + id::Int + agents::Vector{Agent} = Vector() + characteristics::Dict{Agent,Symbol} = Dict() # special agents having specific roles, e.g. :lead for coalition leaders +end + +struct TopologyNeighbor + address::AgentAddress + description::AgentDescription + characteristic::Symbol + + function TopologyNeighbor(address::AgentAddress, description::AgentDescription, characteristic::Symbol=:nothing) + new(address, description, characteristic) + end +end + +@kwdef struct Topology + tid::Symbol + graph::MetaGraph + connectors::Vector{Tuple{Symbol,TopologyNeighbor}} = Vector() # connection type to connector + connections::Vector{Tuple{Symbol,Topology}} = Vector() # tid to connection type +end + +function set_characteristic!(topology::Topology, nid::Int64, agent::Agent, characteristic::Symbol) + node = topology.graph[nid] + node.characteristics[agent] = characteristic +end + +@enum State begin + NORMAL # normal neighbor + INACTIVE # neighbor link exists but link is not active (could be activated/used) + BROKEN # neighbor link exists but link is not usable (can not be activated) + UNKNOWN # = nothing + EXT_CONNECTION # external connection +end + +@kwdef mutable struct TopologyService + tid_to_state_to_neighbors::Dict{Symbol,Dict{State,Vector{TopologyNeighbor}}} = Dict() # tid to (edge state to agents) + tid_to_connectors::Dict{Symbol,Vector{Tuple{Symbol, TopologyNeighbor}}} = Dict() # tid to (connection type to connected agents) + tid_to_node_id::Dict{Symbol,Int} = Dict() # tid to id of the node + marked_connector_for::Vector{Symbol} = Vector() + tid_to_characteristic::Dict{Symbol,Symbol} = Dict() +end + +function service_node_id(service::TopologyService, tid::Symbol=:default) + if !haskey(service.tid_to_node_id, tid) + throw(ArgumentError("Tid $tid is unknown!")) + end + return service.tid_to_node_id[tid] +end + +function _has_characteristic(characteristic::Symbol, has_characteristic::Union{Symbol,Vector{Symbol}}) + # no characteristic was demanded or the characteristic is included in the demanded ones + return characteristic == has_characteristic || isa(has_characteristic, Vector) && + (length(has_characteristic) == 0 || characteristic ∈ has_characteristic) +end + +function neighbors(service::TopologyService, tid::Symbol=:default, state::State=NORMAL; has_characteristic::Union{Symbol,Vector{Symbol}}=Vector{Symbol}(), include_connectors::Vector{Symbol}=Vector{Symbol}(), match_func::Function=(desc)->true) + if haskey(service.tid_to_state_to_neighbors, tid) + return vcat([n.address for n in get(service.tid_to_state_to_neighbors[tid], state, Vector()) if _has_characteristic(n.characteristic, has_characteristic) && match_func(n)], + [t[2].address for t in service.tid_to_connectors[tid] if t[1] in include_connectors && match_func(t)]) + end + throw(ArgumentError("No neighbors found for tid=$tid")) +end + +function connectors(service::TopologyService, tid::Symbol=:default; include_connectors::Vector{Symbol}=Vector{Symbol}(), match_func::Function=(desc)->true) + if haskey(service.tid_to_state_to_neighbors, tid) + return [t[2].address for t in service.tid_to_connectors[tid] if (t[1] in include_connectors || length(include_connectors) == 0) && match_func(t)] + end + throw(ArgumentError("No neighbors found for tid=$tid")) +end + +function connection_types(service::TopologyService, tid::Symbol=:default) + if haskey(service.tid_to_state_to_neighbors, tid) + return [t[1] for t in service.tid_to_connectors[tid]] + end + throw(ArgumentError("No neighbors found for tid=$tid")) +end + +function characteristic(service::TopologyService, tid::Symbol=:default) + if haskey(service.tid_to_characteristic, tid) + return service.tid_to_characteristic[tid] + end + return :nothing +end + +function _create_meta_graph_with(graph::AbstractGraph) + vertices_description = [i => Node(id=i) for i in vertices(graph)] + edges_description = [(e.src, e.dst) => NORMAL for e in edges(graph)] + + return MetaGraph(graph, vertices_description, edges_description) +end + +""" + complete_topology(number_of_nodes) + +Create a fully-connected topology. +""" +function complete_topology(number_of_nodes::Int; tid::Symbol=:default)::Topology + graph = complete_graph(number_of_nodes) + return Topology(tid=tid, graph=_create_meta_graph_with(graph)) +end + +""" + star_topology(number_of_nodes) + +Create a star topology. +""" +function star_topology(number_of_nodes::Int; tid::Symbol=:default) + graph = star_graph(number_of_nodes) + return Topology(tid=tid, graph=_create_meta_graph_with(graph)) +end + +""" + cycle_topology(number_of_nodes) + +Create a cycle topology. +""" +function cycle_topology(number_of_nodes::Int; tid::Symbol=:default) + graph = cycle_graph(number_of_nodes) + return Topology(tid=tid, graph=_create_meta_graph_with(graph)) +end + +""" + graph_topology(graph) + +Create a topology based on a Graphs.jl (abstract) graph. +""" +function graph_topology(graph::AbstractGraph; tid::Symbol=:default) + return Topology(tid=tid, graph=_create_meta_graph_with(graph)) +end + +""" + add_edge!(topology, node_id_from, node_id_to, directed=false) + +Add an edge to the topology from `node_id_from` to `node_id_to`. If `directed` is true +a directed edge is added, otherwise an undirected edge is added. +""" +function add_edge!(topology::Topology, node_id_from::Int, node_id_to::Int, state::State=NORMAL; directed::Bool=false) + if directed + topology.graph[node_id_from, node_id_to] = state + else + topology.graph[node_id_to, node_id_from] = state + topology.graph[node_id_from, node_id_to] = state + end +end + +""" + remove_edge!(topology::Topology, node_id_from::Int, node_id_to::Int) + +Remove the edge between `node_id_from` and `node_id_to`. +""" +function remove_edge!(topology::Topology, node_id_from::Int, node_id_to::Int) + return rem_edge!(topology.graph, node_id_from, node_id_to) +end + +""" + remove_node!(topology::Topology, node_id::Int) + +Remove the node with the id `node_id`. +""" +function remove_node!(topology::Topology, node_id::Int) + return rem_vertex!(topology.graph, node_id) +end + +""" + add_node!(topology, agents::Agent...)::Int + +Add a node to the topology with a list (or a single) of agents attached. +""" +function add_node!(topology::Topology, agents::Agent...; id::Union{Int,Nothing}=nothing)::Int + vid = isnothing(id) ? nv(topology.graph) + 1 : id + topology.graph[vid] = Node(id=vid, agents=[a for a in agents]) + return vid +end + +""" + set_as_connectors!(topology::Topology, agents..., connector_type::Symbol=:default) + +Set `agents` as connectors (has to be part of the topology) +""" +function set_as_connector!(topology::Topology, agents...; connector_type::Symbol=:default) + for a in agents + push!(topology.connectors, (connector_type, TopologyNeighbor(address(a), description(a)))) + end +end + +function mark_as_connector!(agent::Agent, connector_type::Symbol=:default) + ts = service_of_type(agent, TopologyService, TopologyService()) + push!(ts.marked_connector_for, connector_type) +end + +""" + connect(topology_one::Topology, topology_two::Topology, connection_type::Symbol; directed::Bool=false) + +Connect two topologies on all connectors identified by connection_type. +""" +function connect_topologies!(topology_one::Topology, topology_two::Topology, connection_type::Symbol=:default; directed::Bool=false) + if directed + push!(topology_one.connections, (connection_type, topology_two)) + _build_neighborhoods_and_inject(topology_one) + else + push!(topology_two.connections, (connection_type, topology_one)) + push!(topology_one.connections, (connection_type, topology_two)) + _build_neighborhoods_and_inject(topology_one) + _build_neighborhoods_and_inject(topology_two) + end +end + +""" + set_state!(topology::Topology, node_id_from::Int, node_id_to::Int, state::State) + +Set the state of the state of the edge `(node_id_from, node_id_to)` to `state`. +""" +function set_edge_state!(topology::Topology, node_id_from::Int, node_id_to::Int, state::State, include_other_direction=true) + topology.graph[node_id_from, node_id_to] = state + if include_other_direction + if has_edge(topology.graph, node_id_to, node_id_from) + topology.graph[node_id_to, node_id_from] = state + end + end +end + +function _build_connectors_list_for(topology, agent) + connectors_for_agent = [] + for (type, other_topo) in topology.connections + # check whether agent is a connector for the connection + for (c_type, neighbor) in topology.connectors + if type == c_type && uid(agent) == neighbor.description.uid + # it is a connector + # now find the fitting connectors in the connected topo + for (other_c_type, other_neighbor) in other_topo.connectors + if type == other_c_type + push!(connectors_for_agent, (type, other_neighbor)) + end + end + end + end + end + return connectors_for_agent +end + +function _characteristic_for(node::Node, agent::Agent) + return get!(node.characteristics, agent, :nothing) +end + +function _build_neighborhoods_and_inject(topology::Topology; build_connected=true) + # 2nd pass, build the neighborhoods and add it to agents + for label in labels(topology.graph) + node = topology.graph[label] + state_to_neighbors::Dict{State,Vector{TopologyNeighbor}} = Dict{State,Vector{TopologyNeighbor}}() + for n_label in neighbor_labels(topology.graph, label) + n_node = topology.graph[n_label] + state = topology.graph[node.id, n_node.id] + neighbor_addresses = get!(state_to_neighbors, state, Vector()) + append!(neighbor_addresses, [TopologyNeighbor(address(agent), description(agent), _characteristic_for(n_node, agent)) for agent in n_node.agents]) + end + for agent in node.agents + # also include agents from your own node (not you!) + state_to_same = deepcopy(state_to_neighbors) + for other_agent in node.agents + if aid(agent) != aid(other_agent) + neighbors = get!(state_to_same, NORMAL, Vector()) + push!(neighbors, TopologyNeighbor(address(other_agent), description(other_agent), _characteristic_for(node, other_agent))) + end + end + topology_service = service_of_type(agent, TopologyService, TopologyService()) + topology_service.tid_to_state_to_neighbors[topology.tid] = state_to_same + topology_service.tid_to_node_id[topology.tid] = node.id + topology_service.tid_to_characteristic[topology.tid] = _characteristic_for(node, agent) + + # look for marks and transfer to topology + for type in topology_service.marked_connector_for + if (type, description(agent)) ∉ [(c[1], c[2].description) for c in topology.connectors] + push!(topology.connectors, (type, TopologyNeighbor(address(agent), description(agent)))) + end + end + # search for connection agents + connectors_for_agent = _build_connectors_list_for(topology, agent) + topology_service.tid_to_connectors[topology.tid] = connectors_for_agent + end + end + if build_connected + for (_, topo) in topology.connections + _build_neighborhoods_and_inject(topo, build_connected=false) + end + end +end + +""" + create_topology(create_runnable::Function; tid::Symbol=:default, directed::Bool=false) + +Create a topology using the `create_runnable` function which is a one-argument +function with an initially empty topology as argument. + +# Example +```julia +topology = create_topology() do topology + agent = register(container, TopologyAgent()) + agent2 = register(container, TopologyAgent()) + agent3 = register(container, TopologyAgent()) + n1 = add_node!(topology, agent) + n2 = add_node!(topology, agent2) + n3 = add_node!(topology, agent3) + add_edge!(topology, n1, n2) + add_edge!(topology, n1, n3) +end +``` +""" +function create_topology(create_runnable::Function; tid::Symbol=:default, directed::Bool=false) + topology = Topology(tid=tid, graph=_create_meta_graph_with(directed ? DiGraph() : Graph())) + create_runnable(topology) + _build_neighborhoods_and_inject(topology) + return topology +end + +""" + modify_topology(modify_runnable::Functino, topology::Topology) + +Modify a topology using the `modify_runnable` function which is a one-argument +function with the provided topology as argument. + +# Example +```julia +modify_topology(my_topology) do topology + agent = register(container, TopologyAgent()) + agent2 = register(container, TopologyAgent()) + agent3 = register(container, TopologyAgent()) + n1 = add_node!(topology, agent) + n2 = add_node!(topology, agent2) + n3 = add_node!(topology, agent3) + add_edge!(topology, n1, n2) + add_edge!(topology, n1, n3) +end +``` +""" +function modify_topology(modify_runnable::Function, topology::Topology) + modify_runnable(topology) + _build_neighborhoods_and_inject(topology) + return topology +end + +""" + per_node(assign_runnable, topology) + +Loops over the nodes of the `topology`, calls `assign_runnable` on every node to enable the caller +to populate the node. After the loop finished the neighborhoods are created and injected into the agent. + +# Example +```julia +per_node(topology) do node + add!(node, register(container, TopologyAgent())) +end +``` +""" +function per_node(assign_runnable::Function, topology::Topology) + # 1st pass, let the user assign the agents + for label in labels(topology.graph) + node = topology.graph[label] + assign_runnable(node) + end + _build_neighborhoods_and_inject(topology) + return topology +end + + +function auto_assign!(topology::Topology, agents) + index_to_label = collect(labels(topology.graph)) + for (i, agent) in enumerate(agents) + label = index_to_label[(((i-1)%length(index_to_label))+1)] + node = topology.graph[label] + add!(node, agent) + end + _build_neighborhoods_and_inject(topology) + return topology +end + +""" + auto_assign(topology, container) + +Assign all agents of the `container` to the nodes of the `topology`. The agents are assigned +to the nodes in the order of the nodes in the graph. +""" +function auto_assign!(topology::Topology, container::ContainerInterface) + return auto_assign!(topology::Topology, agents(container)) +end + +""" + add!(node, agent::Agent...) + +Add an `agents` to the `node`. +""" +function add!(node::Node, agents::Agent...) + for a in agents + push!(node.agents, a) + end +end + +""" + assign_agent(assign_condition::Function, topology::Topology, container::ContainerInterface) + +Assign all agents of the `container` to the nodes based on the given `assign_condition`, this condition +takes as `Agent` and a `Node` (node.id for the identifier of the node) and shall return a boolean indicating +whether the agent shall be assigned to the node. +""" +function assign_agents!(assign_condition::Function, topology::Topology, container::ContainerInterface) + per_node(topology) do node + for agent in agents(container) + if assign_condition(agent, node) + add!(node, agent) + end + end + end +end + +""" + choose_agent(choose_agent_function::Function, topology::Topology) + +Choose the agents, which shall be assigned to the nodes. For this the `choose_agent_function` has to be provided. This +function expects `Node` as argument and shall return an `Agent` or `Agent...`. The returned agent will be assigned to the node. +""" +function choose_agents!(choose_agent_function::Function, topology::Topology) + per_node(topology) do node + agent = choose_agent_function(node) + add!(node, agent) + end +end + +""" + topology_neighbors(agent::Agent; tid::Symbol=:default, state::State=NORMAL)::Vector{AgentAddress} + +Retrieve the neighbors of the `agent`, represented by their addresses. These vaues will be +updated when a topology is applied using `per_node` or `create_topology`. +""" +function topology_neighbors(agent::Agent; tid::Symbol=:default, state::State=NORMAL, has_characteristic::Union{Symbol,Vector{Symbol}}=Vector{Symbol}(), include_connectors::Vector{Symbol}=Vector{Symbol}(), match_func::Function=(desc)->true)::Vector{AgentAddress} + return neighbors(service_of_type(agent, TopologyService, TopologyService()), tid, state, has_characteristic=has_characteristic, include_connectors=include_connectors, match_func=match_func) +end + +function topology_neighbors(role::Role; tid::Symbol=:default, state::State=NORMAL, has_characteristic::Union{Symbol,Vector{Symbol}}=Vector{Symbol}(), include_connectors::Vector{Symbol}=Vector{Symbol}(), match_func::Function=(desc)->true)::Vector{AgentAddress} + return neighbors(service_of_type(role.context.agent, TopologyService, TopologyService()), tid, state, has_characteristic=has_characteristic, include_connectors=include_connectors, match_func=match_func) +end + +function topology_service(role::Role) + return service_of_type(role.context.agent, TopologyService, TopologyService()) +end + +""" + topology_node_id(agent::Agent; tid::Symbol=:default)::Int + +Retrieve the node id the `agent` is assigned to. +""" +function topology_node_id(agent::Agent; tid::Symbol=:default)::Int + return service_node_id(service_of_type(agent, TopologyService, TopologyService()), tid) +end + +function topology_node_id(role::Role; tid::Symbol=:default)::Int + return service_node_id(service_of_type(role.context.agent, TopologyService, TopologyService()), tid) +end + +""" + topology_connectors(agent::Agent; tid::Symbol=:default, state::State=NORMAL, include_connectors::Vector{Symbol}=Vector{Symbol}())::Vector{AgentAddress} + +Retrieve the connectors of the `agent`, represented by their addresses. These vaues will be +updated when a topology is applied using `per_node` or `create_topology`. +""" +function topology_connectors(agent::Agent; tid::Symbol=:default, include_connectors::Vector{Symbol}=Vector{Symbol}(), match_func::Function=(desc)->true)::Vector{AgentAddress} + return connectors(service_of_type(agent, TopologyService, TopologyService()), tid, include_connectors=include_connectors, match_func=match_func) +end + +function topology_connectors(role::Role; tid::Symbol=:default, include_connectors::Vector{Symbol}=Vector{Symbol}(), match_func::Function=(desc)->true)::Vector{AgentAddress} + return connectors(service_of_type(role.context.agent, TopologyService, TopologyService()), tid, include_connectors=include_connectors, match_func=match_func) +end + + +""" + topology_connection_types(agent::Agent; tid::Symbol=:default, include_connectors::Vector{Symbol}=Vector{Symbol}())::Vector{AgentAddress} + +Retrieve the connection_types for connectors used available to the `agent`, represented by their addresses. These vaues will be +updated when a topology is applied using `per_node` or `create_topology`. +""" +function topology_connection_types(agent::Agent; tid::Symbol=:default)::Vector{Symbol} + return connection_types(service_of_type(agent, TopologyService, TopologyService()), tid) +end + +function topology_connection_types(role::Role; tid::Symbol=:default)::Vector{Symbol} + return connection_types(service_of_type(role.context.agent, TopologyService, TopologyService()), tid) +end + +function topology_characteristic(agent::Agent; tid::Symbol=:default)::Symbol + return characteristic(service_of_type(agent, TopologyService, TopologyService()), tid) +end + +function topology_characteristic(role::Role; tid::Symbol=:default)::Symbol + return characteristic(service_of_type(role.context.agent, TopologyService, TopologyService()), tid) +end + + +# Graphs API calls forwarded to Topology +function Graphs.edges(topology::Topology) + return edges(topology.graph) +end + +function Graphs.edgetype(topology::Topology) + return edgetype(topology.graph) +end + +function Graphs.vertices(topology::Topology) + return vertices(topology.graph) +end + +function Graphs.has_edge(topology::Topology, s::Any, d::Any) + return has_edge(topology.graph, s, d) +end + +function Graphs.has_vertex(topology::Topology, v::Any) + return has_vertex(topology.graph, v) +end + +function Graphs.inneighbors(topology::Topology, v::Any) + return inneighbors(topology.graph, v) +end + +function Graphs.outneighbors(topology::Topology, v::Any) + return outneighbors(topology.graph, v) +end + +function Graphs.is_directed(topology::Topology) + return is_directed(topology.graph) +end + +function Graphs.ne(topology::Topology) + return ne(topology.graph) +end + +function Graphs.nv(topology::Topology) + return nv(topology.graph) +end + +""" + topology_to_aid_graph(topology::Topology)::AbstractGraph + +Convert the topology graph to an aid based graph, where every node is representing exactly one agent. +""" +function topology_to_aid_graph(topology::Topology) + vertex_description::Vector{Pair{String,Agent}} = [] + edges_description::Vector{Pair{Tuple{String,String},State}} = [] + graph = SimpleGraph() + aid_to_vertex = Dict() + for vertex in vertices(topology.graph) + label = label_for(topology.graph, vertex) + node = topology.graph[label] + for agent in node.agents + add_vertex!(graph) + push!(vertex_description, aid(agent) => agent) + aid_to_vertex[aid(agent)] = nv(graph) + end + for agent in node.agents + for agent_two in node.agents + if agent == agent_two + continue + end + if !has_edge(graph, aid_to_vertex[aid(agent)], aid_to_vertex[aid(agent_two)]) + add_edge!(graph, aid_to_vertex[aid(agent)], aid_to_vertex[aid(agent_two)]) + push!(edges_description, (aid(agent), aid(agent_two)) => NORMAL) + end + end + end + end + for edge in edges(topology.graph) + edge_src_code = edge.src + edge_dst_code = edge.dst + edge_src_label = label_for(topology.graph, edge_src_code) + edge_dst_label = label_for(topology.graph, edge_dst_code) + edge_src_node = topology.graph[edge_src_label] + edge_dst_node = topology.graph[edge_dst_label] + for agent_src in edge_src_node.agents + for agent_dst in edge_dst_node.agents + if !has_edge(graph, aid_to_vertex[aid(agent_src)], aid_to_vertex[aid(agent_dst)]) + add_edge!(graph, aid_to_vertex[aid(agent_src)], aid_to_vertex[aid(agent_dst)]) + push!(edges_description, (aid(agent_src), aid(agent_dst)) => topology.graph[edge_src_label, edge_dst_label]) + end + end + end + end + return MetaGraph(graph, vertex_description, edges_description) +end diff --git a/src/visualization.jl b/src/visualization.jl new file mode 100644 index 00000000..2d731ec1 --- /dev/null +++ b/src/visualization.jl @@ -0,0 +1,8 @@ +export plot_node_topology, plot_multi_agent_topology, show_communication_data, plot_world, plot_agents, plot_recordings + +function plot_node_topology end +function plot_multi_agent_topology end +function show_communication_data end +function plot_world end +function plot_agents end +function plot_recordings end diff --git a/src/world/core.jl b/src/world/core.jl deleted file mode 100644 index d6579420..00000000 --- a/src/world/core.jl +++ /dev/null @@ -1,54 +0,0 @@ -export World, Space, Position, Position2D, Area2D, location, move, initialize, initialized - -abstract type Position end -abstract type Space{P<:Position} end - -struct Position2D <: Position - x::Real - y::Real -end - -@kwdef struct Area2D <: Space{Position2D} - width::Real - height::Real - to_position::Dict{String,Position2D} = Dict() -end - -@kwdef struct World{S<:Space} - space::S = Area2D(width=10, height=10) - initialized::Bool = false -end - -function location(space::Space{P}, agent::Agent)::P where {P<:Position} - throw("Position on the space $space not defined!") -end - -function location(space::Area2D, agent::Agent)::Position2D - return space.to_position[aid(agent)] -end - -function move(space::Space{P}, agent::Agent, position::P) where {P<:Position} - throw("Move on the space $space not defined!") -end - -function move(space::Area2D, agent::Agent, position::Position2D) - space.to_position[aid(agent)] = position -end - -function initialize(space::Space, agents::Vector{A}) where {A<:Agent} - throw("Initialization for $space is not defined!") -end - -function initialize(space::Area2D, agents::Vector{A}) where {A<:Agent} - for agent in agents - space.to_position[aid(agent)] = Position2D(rand() * space.width, rand() * space.height) - end -end - -function initialize(world::World{S}, agents::Vector{A}) where {S<:Space} where {A<:Agent} - initialize(world.space, agents) -end - -function initialized(world::World) - return world.initialized -end diff --git a/src/world/topology.jl b/src/world/topology.jl deleted file mode 100644 index 3511b56b..00000000 --- a/src/world/topology.jl +++ /dev/null @@ -1,275 +0,0 @@ -export complete_topology, star_topology, cycle_topology, graph_topology, per_node, add!, topology_neighbors, create_topology, add_node!, add_edge!, Topology, modify_topology, choose_agent, assign_agent, NORMAL, BROKEN, INACTIVE, set_edge_state!, remove_edge!, remove_node! - -using MetaGraphsNext -using Graphs -import Graphs.add_edge! - -@kwdef struct Node - id::Int - agents::Vector{Agent} = Vector() -end - -struct Topology - graph::MetaGraph -end - -@enum State begin - NORMAL # normal neighbor - INACTIVE # neighbor link exists but link is not active (could be activated/used) - BROKEN # neighbor link exists but link is not usable (can not be activated) -end - -@kwdef mutable struct TopologyService - state_to_neighbors::Dict{State,Vector{AgentAddress}} = Dict() -end - -function neighbors(service::TopologyService, state::State=NORMAL) - return get(service.state_to_neighbors, state, Vector()) -end - -function _create_meta_graph_with(graph::AbstractGraph) - vertices_description = [i => Node(id=i) for i in vertices(graph)] - edges_description = [(e.src, e.dst) => NORMAL for e in edges(graph)] - - return MetaGraph(graph, vertices_description, edges_description) -end - -""" - complete_topology(number_of_nodes) - -Create a fully-connected topology. -""" -function complete_topology(number_of_nodes::Int)::Topology - graph = complete_graph(number_of_nodes) - return Topology(_create_meta_graph_with(graph)) -end - -""" - star_topology(number_of_nodes) - -Create a star topology. -""" -function star_topology(number_of_nodes::Int) - graph = star_graph(number_of_nodes) - return Topology(_create_meta_graph_with(graph)) -end - -""" - cycle_topology(number_of_nodes) - -Create a cycle topology. -""" -function cycle_topology(number_of_nodes::Int) - graph = cycle_graph(number_of_nodes) - return Topology(_create_meta_graph_with(graph)) -end - -""" - graph_topology(graph) - -Create a topology based on a Graphs.jl (abstract) graph. -""" -function graph_topology(graph::AbstractGraph) - return Topology(_create_meta_graph_with(graph)) -end - -""" - add_edge!(topology, node_id_from, node_id_to, directed=false) - -Add an edge to the topology from `node_id_from` to `node_id_to`. If `directed` is true -a directed edge is added, otherwise an undirected edge is added. -""" -function add_edge!(topology::Topology, node_id_from::Int, node_id_to::Int, state::State=NORMAL; directed::Bool=false) - if directed - topology.graph[node_id_from, node_id_to] = state - else - topology.graph[node_id_to, node_id_from] = state - topology.graph[node_id_from, node_id_to] = state - end -end - -""" - remove_edge!(topology::Topology, node_id_from::Int, node_id_to::Int) - -Remove the edge between `node_id_from` and `node_id_to`. -""" -function remove_edge!(topology::Topology, node_id_from::Int, node_id_to::Int) - return rem_edge!(topology.graph, node_id_from, node_id_to) -end - -""" - remove_node!(topology::Topology, node_id::Int) - -Remove the node with the id `node_id`. -""" -function remove_node!(topology::Topology, node_id::Int) - return rem_vertex!(topology.graph, node_id) -end - -""" - add_node!(topology, agents::Agent...)::Int - -Add a node to the topology with a list (or a single) of agents attached. -""" -function add_node!(topology::Topology, agents::Agent...; id::Union{Int,Nothing}=nothing)::Int - vid = isnothing(id) ? nv(topology.graph) + 1 : id - topology.graph[vid] = Node(vid, [a for a in agents]) - return vid -end - -""" - set_state!(topology::Topology, node_id_from::Int, node_id_to::Int, state::State) - -Set the state of the state of the edge `(node_id_from, node_id_to)` to `state`. -""" -function set_edge_state!(topology::Topology, node_id_from::Int, node_id_to::Int, state::State) - topology.graph[node_id_from, node_id_to] = state -end - -""" - create_topology(create_runnable)::Topology - -Create a topology using the `create_runnable` function which is a one-argument -function with an initially empty topology as argument. - -# Example -```julia -topology = create_topology() do topology - agent = register(container, TopologyAgent()) - agent2 = register(container, TopologyAgent()) - agent3 = register(container, TopologyAgent()) - n1 = add_node!(topology, agent) - n2 = add_node!(topology, agent2) - n3 = add_node!(topology, agent3) - add_edge!(topology, n1, n2) - add_edge!(topology, n1, n3) -end -``` -""" -function create_topology(create_runnable::Function; directed::Bool=false) - topology = Topology(_create_meta_graph_with(directed ? DiGraph() : Graph())) - create_runnable(topology) - _build_neighborhoods_and_inject(topology) - return topology -end - -""" - modify_topology(modify_runnable::Functino, topology::Topology) - -Modify a topology using the `modify_runnable` function which is a one-argument -function with the provided topology as argument. - -# Example -```julia -modify_topology(my_topology) do topology - agent = register(container, TopologyAgent()) - agent2 = register(container, TopologyAgent()) - agent3 = register(container, TopologyAgent()) - n1 = add_node!(topology, agent) - n2 = add_node!(topology, agent2) - n3 = add_node!(topology, agent3) - add_edge!(topology, n1, n2) - add_edge!(topology, n1, n3) -end -``` -""" -function modify_topology(modify_runnable::Function, topology::Topology) - modify_runnable(topology) - _build_neighborhoods_and_inject(topology) - return topology -end - -function _build_neighborhoods_and_inject(topology::Topology) - # 2nd pass, build the neighborhoods and add it to agents - for label in labels(topology.graph) - node = topology.graph[label] - state_to_neighbors::Dict{State,Vector{AgentAddress}} = Dict{State,Vector{AgentAddress}}() - for n_label in neighbor_labels(topology.graph, label) - n_node = topology.graph[n_label] - state = topology.graph[node.id, n_node.id] - neighbor_addresses = get!(state_to_neighbors, state, Vector()) - append!(neighbor_addresses, [address(agent) for agent in n_node.agents]) - end - for agent in node.agents - topology_service = service_of_type(agent, TopologyService, TopologyService()) - topology_service.state_to_neighbors = state_to_neighbors - end - end -end - -""" - per_node(assign_runnable, topology) - -Loops over the nodes of the `topology`, calls `assign_runnable` on every node to enable the caller -to populate the node. After the loop finished the neighborhoods are created and injected into the agent. - -# Example -```julia -per_node(topology) do node - add!(node, register(container, TopologyAgent())) -end -``` -""" -function per_node(assign_runnable::Function, topology::Topology) - # 1st pass, let the user assign the agents - for label in labels(topology.graph) - node = topology.graph[label] - assign_runnable(node) - end - _build_neighborhoods_and_inject(topology) -end - -""" - add!(node, agent::Agent...) - -Add an `agents` to the `node`. -""" -function add!(node::Node, agents::Agent...) - for a in agents - push!(node.agents, a) - end -end - -""" - assign_agent(assign_condition::Function, topology::Topology, container::ContainerInterface) - -Assign all agents of the `container` to the nodes based on the given `assign_condition`, this condition -takes as `Agent` and a `Node` (node.id for the identifier of the node) and shall return a boolean indicating -whether the agent shall be assigned to the node. -""" -function assign_agent(assign_condition::Function, topology::Topology, container::ContainerInterface) - per_node(topology) do node - for agent in agents(container) - if assign_condition(agent, node) - add!(node, agent) - end - end - end -end - -""" - choose_agent(choose_agent_function::Function, topology::Topology) - -Choose the agents, which shall be assigned to the nodes. For this the `choose_agent_function` has to be provided. This -function expects `Node` as argument and shall return an `Agent` or `Agent...`. The returned agent will be assigned to the node. -""" -function choose_agent(choose_agent_function::Function, topology::Topology) - per_node(topology) do node - agent = choose_agent_function(node) - add!(node, agent) - end -end - -""" - topology_neighbors(agent) - -Retrieve the neighbors of the `agent`, represented by their addresses. These vaues will be -updated when a topology is applied using `per_node` or `create_topology`. -""" -function topology_neighbors(agent::Agent, state::State=NORMAL)::Vector{AgentAddress} - return neighbors(service_of_type(agent, TopologyService, TopologyService()), state) -end - -function topology_neighbors(role::Role, state::State=NORMAL)::Vector{AgentAddress} - return neighbors(service_of_type(role.context.agent, TopologyService, TopologyService()), state) -end \ No newline at end of file diff --git a/test/agent_modeling_tests.jl b/test/agent_modeling_tests.jl index 5521a37f..fc57540c 100644 --- a/test/agent_modeling_tests.jl +++ b/test/agent_modeling_tests.jl @@ -8,12 +8,12 @@ import Mango.on_step counter::Real end -function on_step(agent::ModellingAgent, world::World, clock::Clock, step_size_s::Real) +function on_step(agent::ModellingAgent, env::Environment, clock::Clock, step_size_s::Real) agent.counter += step_size_s end @testset "TestAgentIsStepped" begin - container = create_simulation_container(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) + container = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) agent1 = ModellingAgent(0) agent2 = ModellingAgent(0) register(container, agent1) @@ -30,12 +30,12 @@ end counter::Real end -function on_step(role::ModellingRole, world::World, clock::Clock, step_size_s::Real) +function on_step(role::ModellingRole, env::Environment, clock::Clock, step_size_s::Real) role.counter += step_size_s end @testset "TestAgentIsSteppedRole" begin - container = create_simulation_container(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) + container = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) agent = ModellingAgent(0) role = ModellingRole(0) register(container, agent) @@ -54,14 +54,14 @@ end position::Position2D end -function on_step(agent::ModellingMovingAgent, world::World, clock::Clock, step_size_s::Real) - agent.prev_position = location(world.space, agent) - move(world.space, agent, agent.target) - agent.position = location(world.space, agent) +function on_step(agent::ModellingMovingAgent, env::Environment, clock::Clock, step_size_s::Real) + agent.prev_position = location(env.space, agent) + move(env.space, agent, agent.target) + agent.position = location(env.space, agent) end @testset "TestAgentStepPosition" begin - container = create_simulation_container(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) + container = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) given_initial = Position2D(-1, -1) given_target = Position2D(1, 1) agent = ModellingMovingAgent(given_target, given_initial, given_initial) diff --git a/test/agent_tests.jl b/test/agent_tests.jl index 1873d291..fbbacb82 100644 --- a/test/agent_tests.jl +++ b/test/agent_tests.jl @@ -36,7 +36,7 @@ end register(container, agent1) register(container, agent2) - wait(send_message(container, "Hello Roles, this is RSc!", AgentAddress(aid=agent2.aid))) + wait(send_message(container, "Hello Roles, this is RSc!", AgentAddress(aid=aid(agent2)))) @test agent2.role_handler.roles[1] === role1 @test agent2.counter == 10 @@ -57,7 +57,7 @@ end register(container, agent1) register(container, agent2) - wait(send_message(container, "Hello Roles, this is RSc!", AgentAddress(aid=agent2.aid))) + wait(send_message(container, "Hello Roles, this is RSc!", AgentAddress(aid=aid(agent2)))) @test agent2.role_handler.roles[1] === role1 @test agent2.role_handler.roles[1].counter == 15 @@ -82,7 +82,7 @@ end register(container, agent1) register(container, agent2) - wait(send_message(agent2, "Hello Roles, this is RSc!", AgentAddress(aid=agent1.aid))) + wait(send_message(agent2, "Hello Roles, this is RSc!", AgentAddress(aid=aid(agent1)))) @test agent2.role_handler.roles[1] === role1 @test agent2.role_handler.roles[1].invoked @@ -95,7 +95,7 @@ end register(container, agent1) register(container, agent2) - wait(send_message(agent1, "Hello Agents, this is RSc!", AgentAddress(aid=agent2.aid))) + wait(send_message(agent1, "Hello Agents, this is RSc!", AgentAddress(aid=aid(agent2)))) @test agent2.counter == 10 end @@ -107,7 +107,7 @@ end register(container, agent1) register(container, agent2) - wait(send_message(agent1, "Hello Agents, this is RSc!", AgentAddress(aid=agent2.aid); kw=1, kw2=2)) + wait(send_message(agent1, "Hello Agents, this is RSc!", AgentAddress(aid=aid(agent2)); kw=1, kw2=2)) @test agent2.counter == 10 end @@ -123,7 +123,7 @@ end register(container, agent1) register(container, agent2) - wait(send_message(role2, "Hello Roles, this is RSc!", AgentAddress(aid=agent2.aid))) + wait(send_message(role2, "Hello Roles, this is RSc!", AgentAddress(aid=aid(agent2)))) @test agent2.role_handler.roles[1] === role1 @test agent2.counter == 10 @@ -165,11 +165,11 @@ end container = Container() agent1 = MyTrackedAgent(0) - agent2 = MyRespondingAgent(0, AgentAddress(aid=agent1.aid)) + agent2 = MyRespondingAgent(0, AgentAddress(aid=aid(agent1))) register(container, agent1) register(container, agent2) - wait(send_tracked_message(agent1, "Hello Agent, this is DialogRico", AgentAddress(aid=agent2.aid); response_handler=handle_response)) + wait(send_tracked_message(agent1, "Hello Agent, this is DialogRico", AgentAddress(aid=aid(agent2)); response_handler=handle_response)) @test agent2.counter == 10 @test agent1.counter == 1337 @@ -228,6 +228,31 @@ end @test role1.counter == 1111 end +@testset "RoleAgentDialogWithDoSyntaxMultiMsg" begin + container = Container() + agent1 = MyAgent(0) + agent2 = MyAgent(0) + agent3 = MyAgent(0) + role1 = MyTrackedRole(0) + role2 = MyRespondingRole(0) + role3 = MyRespondingRole(0) + add(agent2, role1) + add(agent1, role2) + add(agent3, role3) + register(container, agent1) + register(container, agent2) + register(container, agent3) + + tasks = send_and_handle_answers(role1, "Hello Agent, this is DialogRico", [address(agent1), address(agent3)]) do role, message, meta + role.counter = 1111 + end + for t in tasks + wait(t) + end + + @test role2.counter == 10 + @test role1.counter == 1111 +end @testset "AgentMQTTMessaging" begin broker_addr = InetAddr(ip"127.0.0.1", 1883) @@ -333,4 +358,128 @@ end @test agent_var_var.other == 2 @test agent_var_var.the_float == 3.3 +end + +@testset "TestSenderAddr" begin + meta = Dict("sender_addr" => "sender_addr", "sender_id" => "sender_id", "tracking_id" => "tracking_id") + sa = sender_address(meta) + + @test sa == AgentAddress(aid="sender_id", address="sender_addr") +end + +@agent struct MyExpectingPrepAgent + triggered::Bool = false +end + +@agent struct MySendingPrepAgent end + +function handle_trigger_cp(a::MyExpectingPrepAgent, msg::Any, meta::Any) + a.triggered = true +end + +struct ExpectedMessage end + +@testset "AgentWaitingMessagePreprocessor" begin + + container = Container() + agent1 = MyExpectingPrepAgent() + agent2 = MySendingPrepAgent() + register(container, agent1) + register(container, agent2) + wmp = WaitingMessagePreprocessor(waiting_for_func=() -> [address(agent2)]) + subscribe_message(agent1, (msg, meta) -> typeof(msg) == ExpectedMessage, handle_trigger_cp, preprocessor=wmp) + wait(send_message(agent2, ExpectedMessage(), address(agent1))) + sleep(0.01) + + @test agent1.triggered +end + +@testset "AgentWaitingMessagePreprocessorNoTrig" begin + + container = Container() + agent1 = MyExpectingPrepAgent() + agent2 = MySendingPrepAgent() + agent3 = MySendingPrepAgent() + register(container, agent1) + register(container, agent2) + register(container, agent3) + wmp = WaitingMessagePreprocessor(waiting_for_func=() -> [address(agent2),address(agent3)]) + subscribe_message(agent1, (msg, meta) -> typeof(msg) == ExpectedMessage, handle_trigger_cp, preprocessor=wmp) + wait(send_message(agent2, ExpectedMessage(), address(agent1))) + sleep(0.01) + + @test !agent1.triggered +end +@testset "AgentWaitingMessagePreprocessorMultiTrig" begin + + container = Container() + agent1 = MyExpectingPrepAgent() + agent2 = MySendingPrepAgent() + agent3 = MySendingPrepAgent() + register(container, agent1) + register(container, agent2) + register(container, agent3) + wmp = WaitingMessagePreprocessor(waiting_for_func=() -> [address(agent2),address(agent3)]) + subscribe_message(agent1, (msg, meta) -> typeof(msg) == ExpectedMessage, handle_trigger_cp, preprocessor=wmp) + wait(send_message(agent2, ExpectedMessage(), address(agent1))) + wait(send_message(agent3, ExpectedMessage(), address(agent1))) + sleep(0.01) + + @test agent1.triggered +end + +@testset "AgentWaitingMessagePreprocessorWrongTrig" begin + + container = Container() + agent1 = MyExpectingPrepAgent() + agent2 = MySendingPrepAgent() + agent3 = MySendingPrepAgent() + register(container, agent1) + register(container, agent2) + register(container, agent3) + wmp = WaitingMessagePreprocessor(waiting_for_func=() -> [address(agent2),address(agent3)]) + subscribe_message(agent1, (msg, meta) -> typeof(msg) == ExpectedMessage, handle_trigger_cp, preprocessor=wmp) + wait(send_message(agent2, ExpectedMessage(), address(agent1))) + wait(send_message(agent2, ExpectedMessage(), address(agent1))) + sleep(0.01) + + @test !agent1.triggered +end + + + +@testset "AgentDescriptionUpdate" begin + container = Container() + agent1 = MyAgent(0) + role1 = MyRole(0) + add(agent1, role1) + register(container, agent1) + + update_description(agent1, color=:a, name="a", category=:b) + + @test name(agent1) == "a" + @test category(agent1) == :b + @test color(agent1) == :a + @test name(role1) == "a" + @test category(role1) == :b + @test color(role1) == :a + @test !isnothing(description(role1)) + @test has_role(agent1, MyRole) +end + +@testset "AgentServicesBasics" begin + agent = MyAgent(0) + role = MyRole(0) + add(agent, role) + install_observer(agent, :def) do + return "" + end + install_action(agent, :def) do + return "" + end + + @test action(agent, :def)() == "" + @test observation(agent, :def) == "" + @test action(role, :def)() == "" + @test observation(role, :def) == "" end \ No newline at end of file diff --git a/test/container_tests.jl b/test/container_tests.jl index 395755ce..812dc5ad 100644 --- a/test/container_tests.jl +++ b/test/container_tests.jl @@ -26,7 +26,7 @@ end register(container, agent1) register(container, agent2) - wait(Threads.@spawn send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid))) + wait(Threads.@spawn send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1)))) @test agent1.counter == 10 end @@ -49,7 +49,7 @@ end send_message( container2, "Hello Friends2, this is RSc!", - AgentAddress(aid=agent3.aid, address=InetAddr(ip"127.0.0.1", 2940)) + AgentAddress(aid=aid(agent3), address=InetAddr(ip"127.0.0.1", 2940)) ), ) @@ -90,7 +90,7 @@ end register(container, pong_agent) activate([container, container2]) do - wait(send_message(ping_agent, "Ping", AgentAddress(aid=pong_agent.aid, address=InetAddr(ip"127.0.0.1", 2939)))) + wait(send_message(ping_agent, "Ping", AgentAddress(aid=aid(pong_agent), address=InetAddr(ip"127.0.0.1", 2939)))) wait(Threads.@spawn begin while ping_agent.counter < 5 sleep(1) @@ -132,7 +132,7 @@ end register(container, responding_agent) activate([container, container2]) do - wait(send_tracked_message(tracked_agent, "Hello Agent, this is DialogRico", AgentAddress(aid=responding_agent.aid, address=InetAddr(ip"127.0.0.1", 2939)); + wait(send_tracked_message(tracked_agent, "Hello Agent, this is DialogRico", AgentAddress(aid=aid(responding_agent), address=InetAddr(ip"127.0.0.1", 2939)); response_handler=handle_response)) wait(Threads.@spawn begin while tracked_agent.counter == 0 diff --git a/test/environment_api_tests.jl b/test/environment_api_tests.jl new file mode 100644 index 00000000..d2c94f00 --- /dev/null +++ b/test/environment_api_tests.jl @@ -0,0 +1,60 @@ +using Mango +using Test +using Dates + +@agent struct WorldEventAgent + counter::Real +end +@role struct WorldEventRole + counter::Real +end +function Mango.on_global_event(role::WorldEventRole, event::String) + role.counter += 7 +end +function Mango.on_global_event(agent::WorldEventAgent, event::String) + agent.counter += 7 +end + +struct TestBehavior <: Behavior end + +function Mango.on_step(behavior::TestBehavior, environment::DefaultEnvironment, clock::Clock, step_size_s::Real) + emit_global_event(environment, "Hello Agent, I am the environment") + schedule(environment, InstantTaskData()) do + emit_global_event(environment, "Hello Agent, I am the environment") + end +end + +@testset "TestAgentWorldEvent" begin + world = create_world(DateTime(Millisecond(23)), + communication_sim=SimpleCommunicationSimulation(default_delay_s=0), + behavior=TestBehavior()) + agent1 = WorldEventAgent(0) + register(world, agent1) + agent2 = add_agent_composed_of(world, WorldEventRole(1)) + + stepping_result = step_simulation(world) + @test agent1.counter == 7 + @test agent2[WorldEventRole].counter == 8 + stepping_result = step_simulation(world) + @test agent1.counter == 28 + @test agent2[WorldEventRole].counter == 29 +end + +struct TestPosition <: Position end +struct TestSpace <: Space{TestPosition} end + +@testset "TestAgentSpaceApiNotImplemented" begin + test_space = TestSpace() + agent = WorldEventAgent(12) + @test_throws "Initialization for TestSpace is not defined!" initialize(test_space, [agent]) + @test_throws "Move on the space TestSpace not defined!" move(test_space, agent, TestPosition()) + @test_throws "Position on the space TestSpace not defined!" location(test_space, agent) +end + +@testset "TestNoEnvNoImpl" begin + no_env = NoEnv() + + initialize(no_env, [WorldEventAgent(12)]) + @test_throws "Initialized is not implemented for NoEnv" initialized(no_env) + @test_throws "Emit global event not implemented for NoEnv" emit_global_event(no_env, "") +end \ No newline at end of file diff --git a/test/express_api_tests.jl b/test/express_api_tests.jl index 9ca899f2..78dbb10d 100644 --- a/test/express_api_tests.jl +++ b/test/express_api_tests.jl @@ -175,12 +175,12 @@ end @test aid(express_two) == "agent0" end -@testset "TestRunSimulationContainerExpress" begin +@testset "TestRunWorldExpress" begin # Create agents based on roles express_one = agent_composed_of(ExpressRole(0), ExpressRole(0)) express_two = agent_composed_of(ExpressRole(0), ExpressRole(0)) - result = run_in_simulation(1, express_one, express_two) do container + result = run_in_simulation(1, express_one, express_two) do world wait(send_message(express_one, "TestMessage", address(express_two))) end diff --git a/test/role_tests.jl b/test/role_tests.jl index 4db62f34..76b9331d 100644 --- a/test/role_tests.jl +++ b/test/role_tests.jl @@ -269,4 +269,24 @@ end @testset "TestTypedRoles" begin role_var = MyRoleVar(1) @test role_var.counter == 1 +end + + +@testset "GetRoleByType" begin + agent = RoleTestAgent(0) + role1 = RoleTestRole(0, nothing) + role2 = MyRoleVar(1) + add(agent, role1) + add(agent, role2) + + @test agent[RoleTestRole] == role1 + @test agent[MyRoleVar{Int64}] == role2 +end + +@testset "GetRoleByTypeRoleNotFound" begin + agent = RoleTestAgent(0) + role1 = RoleTestRole(0, nothing) + add(agent, role1) + + @test_throws ArgumentError agent[MyRoleVar] end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 1f1074c0..ecde33c0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,17 +2,20 @@ using Test using Documenter @testset "Mango Tests" begin + include("system_programming_tests.jl") include("datastructure_util_tests.jl") include("scheduler_tests.jl") include("agent_tests.jl") include("role_tests.jl") include("container_tests.jl") include("encode_decode_tests.jl") - include("simulation_container_tests.jl") + include("world_tests.jl") include("examples.jl") include("tcp_protocol_tests.jl") include("agent_modeling_tests.jl") include("express_api_tests.jl") include("topology_tests.jl") + include("environment_api_tests.jl") + include("visualization_tests.jl") doctest(Mango) end \ No newline at end of file diff --git a/test/scheduler_tests.jl b/test/scheduler_tests.jl index 77745b9b..575dcdc6 100644 --- a/test/scheduler_tests.jl +++ b/test/scheduler_tests.jl @@ -14,6 +14,18 @@ import Dates @test result == 10 end +@testset "AgentSchedulerDelayThread" begin + scheduler = Scheduler() + result = 0 + + schedule(scheduler, DelayTaskData(0.1)) do + result = 10 + end + sleep(0.2) + + @test result == 10 +end + @testset "AgentSchedulerPeriodicThread" begin scheduler = Scheduler() result = 0 @@ -68,3 +80,32 @@ end @test result == 10 end + +struct NoClock <: AbstractClock end +struct NoScheduler <: AbstractScheduler end + +function Base.wait(str::String) +end + +@testset "SchedulerExceptionTests" begin + scheduler = Scheduler() + c = NoClock() + @test_throws "Not defined!" Mango.time(c) + @test_throws "Not defined!" seconds_elapsed(c) + + no_scheduler = NoScheduler() + e = Threads.Event() + + wait(no_scheduler, "") + notify(no_scheduler, e) + + @test_throws InvalidStateException clock(no_scheduler) + @test_throws InvalidStateException tasks(no_scheduler) + + e = Threads.Event() + scheduler = SimulationScheduler(clock=Clock(Dates.DateTime(0))) + @async begin + wait(scheduler, e) + end + notify(scheduler, e) +end diff --git a/test/simulation_container_tests.jl b/test/simulation_container_tests.jl deleted file mode 100644 index 159f048b..00000000 --- a/test/simulation_container_tests.jl +++ /dev/null @@ -1,395 +0,0 @@ -using Mango -using Test -using Logging -using Dates - -import Mango.handle_message - - -@agent struct SimAgent - counter::Int -end - -function handle_message(agent::SimAgent, message::Any, meta::AbstractDict) - agent.counter += 10 - if haskey(meta, "test") - agent.counter += 1 - end -end - -@testset "SimulationContainerKwargs" begin - - container = create_simulation_container(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) - agent1 = SimAgent(0) - agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2) - - send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid), test=2) - - stepping_result = step_simulation(container, 1) - - shutdown(container) - - @test agent1.counter == 11 -end - -@testset "SimulationContainerNoProtocolSpecificAddr" begin - - container = create_simulation_container(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) - - @test isnothing(protocol_addr(container)) -end - -@testset "SimulationContainerNoValidTargetCustomAid" begin - - container = create_simulation_container(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) - agent1 = SimAgent(0) - agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2, "a1") - - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid="abc")) - - @test_logs (:warn, "Container $(keys(container.agents)) has no agent with id: abc") min_level = Logging.Warn begin - stepping_result = step_simulation(container, 1) - end - - @test agent1.counter == 0 - @test agent2.counter == 0 - @test aid(agent2) == "a1" -end - -@testset "SimpleInternalSimulationWithoutDelayContainerTest" begin - - container = create_simulation_container(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) - agent1 = SimAgent(0) - agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2) - - send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - - stepping_result = step_simulation(container, 1) - - shutdown(container) - - @test agent1.counter == 10 - @test agent2.counter == 10 - @test container.shutdown -end - -@testset "SimpleInternalSimulationDelayGreaterStepSize" begin - - container = create_simulation_container(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) - agent1 = SimAgent(0) - agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2) - - send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 0 - @test agent2.counter == 0 - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 10 - @test agent2.counter == 10 -end - -@testset "SimpleInternalSimulationDelayMixedGreaterStepSize" begin - - container = create_simulation_container(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) - agent1 = SimAgent(0) - agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2) - - send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 0 - @test agent2.counter == 0 - - send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 10 - @test agent2.counter == 10 - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 20 - @test agent2.counter == 20 -end - -@testset "SimpleInternalSimulationLinkSpecificDelay" begin - - com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_simulation_container(DateTime(Millisecond(0)), communication_sim=com_sim) - agent1 = SimAgent(0) - agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2) - com_sim.delay_s_directed_edge_dict[(nothing, aid(agent1))] = 1 - com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 - - send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 10 - @test agent2.counter == 0 - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 10 - @test agent2.counter == 10 - -end - -@agent struct SimSchedulingAgent - counter::Int - scheduled_counter::Int -end - -function handle_message(agent::SimSchedulingAgent, message::Any, meta::AbstractDict) - agent.counter += 1 -end - -@testset "SimulationWithSpecificDelaysAndScheduledTasks" begin - - com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_simulation_container(DateTime(0), communication_sim=com_sim) - agent1 = SimSchedulingAgent(0, 0) - agent2 = SimSchedulingAgent(0, 0) - register(container, agent1) - register(container, agent2) - com_sim.delay_s_directed_edge_dict[(nothing, aid(agent1))] = 1 - com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 - - schedule(agent1, PeriodicTaskData(0.1)) do - agent1.scheduled_counter += 1 - end - schedule(agent1, InstantTaskData()) do - agent1.scheduled_counter += 100 - end - send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 1 - @test agent1.scheduled_counter == 111 - @test agent2.counter == 0 - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 1 - @test agent1.scheduled_counter == 121 - @test agent2.counter == 1 -end - -@agent struct ComplexSimSchedulingAgent - counter::Int - scheduled_counter::Int -end - -function handle_message(agent::ComplexSimSchedulingAgent, message::Any, meta::AbstractDict) - agent.counter += 1 - schedule(agent, InstantTaskData()) do - agent.scheduled_counter += 100 - end -end - -@testset "SimulationWithSpecificDelaysAndScheduledTasksOnHandle" begin - - com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_simulation_container(DateTime(0), communication_sim=com_sim) - agent1 = ComplexSimSchedulingAgent(0, 0) - agent2 = ComplexSimSchedulingAgent(0, 0) - register(container, agent1) - register(container, agent2) - com_sim.delay_s_directed_edge_dict[(nothing, aid(agent1))] = 1 - com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 - - schedule(agent1, PeriodicTaskData(0.1)) do - agent1.scheduled_counter += 1 - end - send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 1 - @test agent1.scheduled_counter == 111 - @test agent2.counter == 0 - @test agent2.scheduled_counter == 0 - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 1 - @test agent1.scheduled_counter == 121 - @test agent2.counter == 1 - @test agent2.scheduled_counter == 100 -end - -@agent struct MoreComplexSimSchedulingAgent - counter::Int - scheduled_counter::Int -end - -function handle_message(agent::MoreComplexSimSchedulingAgent, message::Any, meta::AbstractDict) - agent.counter += 1 - schedule(agent, InstantTaskData()) do - agent.scheduled_counter += 100 - if message == "Hello Friends, this is RSc!" - reply_to(agent, "ABC", meta) - end - end -end - -@testset "SimulationWithSpecificDelaysWithReplyOnHandle" begin - - com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_simulation_container(DateTime(0), communication_sim=com_sim) - agent1 = MoreComplexSimSchedulingAgent(0, 0) - agent2 = MoreComplexSimSchedulingAgent(0, 0) - register(container, agent1) - register(container, agent2) - com_sim.delay_s_directed_edge_dict[(nothing, aid(agent1))] = 1 - com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 - - schedule(agent1, PeriodicTaskData(0.1)) do - agent1.scheduled_counter += 1 - end - send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid), agent2.aid) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 1 - @test agent1.scheduled_counter == 111 - @test agent2.counter == 1 - @test agent2.scheduled_counter == 100 - - stepping_result = step_simulation(container, 1) - - @test agent1.counter == 1 - @test agent1.scheduled_counter == 121 - @test agent2.counter == 2 - @test agent2.scheduled_counter == 200 -end - -@testset "SimulationWithSpecificDelaysWithReplyOnHandleDiscreteEvent" begin - - com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_simulation_container(DateTime(0), communication_sim=com_sim) - agent1 = MoreComplexSimSchedulingAgent(0, 0) - agent2 = MoreComplexSimSchedulingAgent(0, 0) - register(container, agent1) - register(container, agent2) - com_sim.delay_s_directed_edge_dict[(aid(agent2), aid(agent1))] = 1 - com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 - - schedule(agent1, InstantTaskData()) do - agent1.scheduled_counter += 1 - end - schedule(agent1, InstantTaskData()) do - agent1.scheduled_counter += 1 - end - send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid), agent2.aid) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - - stepping_result = step_simulation(container) - - @test stepping_result.simulation_step_size_s == 0 - @test agent1.counter == 0 - @test agent1.scheduled_counter == 2 - @test agent2.counter == 0 - @test agent2.scheduled_counter == 0 - - stepping_result = step_simulation(container) - - @test stepping_result.simulation_step_size_s == 1 - @test agent1.counter == 1 - @test agent1.scheduled_counter == 102 - @test agent2.counter == 1 - @test agent2.scheduled_counter == 100 - - stepping_result = step_simulation(container) - - @test stepping_result.simulation_step_size_s == 1 - @test agent1.counter == 1 - @test agent1.scheduled_counter == 102 - @test agent2.counter == 2 - @test agent2.scheduled_counter == 200 - - stepping_result = step_simulation(container) - - @test isnothing(stepping_result) - - schedule(agent1, PeriodicTaskData(0.1)) do - agent1.scheduled_counter += 1 - end - schedule(agent1, PeriodicTaskData(3)) do - # nothing - end - - stepping_result = step_simulation(container) - - @test stepping_result.simulation_step_size_s == 0 - @test agent1.counter == 1 - @test agent1.scheduled_counter == 103 - @test agent2.counter == 2 - @test agent2.scheduled_counter == 200 - - stepping_result = step_simulation(container) - - @test stepping_result.simulation_step_size_s == 0.1 - @test agent1.counter == 1 - @test agent1.scheduled_counter == 104 - @test agent2.counter == 2 - @test agent2.scheduled_counter == 200 -end - -import Mango - -@testset "SimulationSchedulerDetermineError" begin - s = SimulationScheduler(clock=Clock(DateTime(0))) - push!(s.queue, Task("")) - - @test_throws "This should not happen! Did you schedule a task with zero sleep time?" Mango.determine_next_event_time_with(s, DateTime(0)) -end - -struct TestTaskSim <: TaskSimulation -end - -@testset "SimulationSchedulerDetermineNoImplent" begin - @test_throws "Please implement determine_next_event_time(...)" Mango.determine_next_event_time(TestTaskSim()) -end - -@testset "SimulationContainerAgentsAreOrdered" begin - container = create_simulation_container(DateTime(0)) - a1 = register(container, SimAgent(0)) - a2 = register(container, SimAgent(1)) - a3 = register(container, SimAgent(2)) - a4 = register(container, SimAgent(3)) - - @test agents(container)[1] == a1 - @test agents(container)[2] == a2 - @test agents(container)[3] == a3 - @test agents(container)[4] == a4 - @test container[aid(a1)] == a1 - @test container[1] == a1 -end \ No newline at end of file diff --git a/test/system_programming_tests.jl b/test/system_programming_tests.jl new file mode 100644 index 00000000..58646d8d --- /dev/null +++ b/test/system_programming_tests.jl @@ -0,0 +1,98 @@ + +using Mango +using Dates + +@agent struct SystemProgrammingAgent + got_it::Bool = false +end + +@role struct SystenProgrammingRole + got_it::Bool = false +end + +@agent struct SystemInitAgent end + +struct MessageSystemProgramming end + +@testset "TestSystemProgrammingAPI" begin + + com_sim = SimpleCommunicationSimulation(default_delay_s=0) + world = create_world(DateTime(0), communication_sim=com_sim) + + spa = register(world, SystemProgrammingAgent()) + sia = register(world, SystemInitAgent()) + + behavior_in(world, on_message=MessageSystemProgramming, agent_types=SystemProgrammingAgent) do agent, message, meta + agent.got_it = true + end + + activate(world) do + send_message(sia, MessageSystemProgramming(), address(spa)) + + discrete_step_until(world, 1) + end + + @test spa.got_it +end + +@testset "TestSystemProgrammingAPIEvent" begin + + com_sim = SimpleCommunicationSimulation(default_delay_s=0) + world = create_world(DateTime(0), communication_sim=com_sim) + + spa = add_agent_composed_of(world, SystenProgrammingRole()) + sia = register(world, SystemInitAgent()) + + behavior_in(world, on_event=MessageSystemProgramming, role_types=SystenProgrammingRole) do role, _,_,_ + role.got_it = true + end + + activate(world) do + emit_event(spa[SystenProgrammingRole], MessageSystemProgramming()) + + discrete_step_until(world, 1) + end + + @test spa[SystenProgrammingRole].got_it +end + +@testset "TestSystemProgrammingAPIGlobalEvent" begin + + com_sim = SimpleCommunicationSimulation(default_delay_s=0) + world = create_world(DateTime(0), communication_sim=com_sim) + spa = register(world, SystemProgrammingAgent()) + sia = register(world, SystemInitAgent()) + + behavior_in(world, on_global_event=MessageSystemProgramming, agent_types=SystemProgrammingAgent) do agent, global_event + agent.got_it = true + end + + activate(world) do + emit_global_event(world.env, MessageSystemProgramming()) + + discrete_step_until(world, 1) + end + + @test spa.got_it +end + +@testset "TestSystemProgrammingAPIRole" begin + + com_sim = SimpleCommunicationSimulation(default_delay_s=0) + world = create_world(DateTime(0), communication_sim=com_sim) + + spa = add_agent_composed_of(world, SystenProgrammingRole()) + sia = register(world, SystemInitAgent()) + + behavior_in(world, on_message=MessageSystemProgramming, role_types=SystenProgrammingRole) do role, message, meta + role.got_it = true + end + + activate(world) do + send_message(sia, MessageSystemProgramming(), address(spa)) + + discrete_step_until(world, 1) + end + + @test spa[SystenProgrammingRole].got_it +end \ No newline at end of file diff --git a/test/topology_tests.jl b/test/topology_tests.jl index d631a11b..d20024a0 100644 --- a/test/topology_tests.jl +++ b/test/topology_tests.jl @@ -44,6 +44,23 @@ end @test topology_neighbors(agents(container)[3]) == [address(agents(container)[1])] end +@testset "TestCreateTopologyMultiAgentNode" begin + container = create_tcp_container("127.0.0.1", 3333) + agent = nothing + + create_topology() do topology + agent = register(container, TopologyAgent()) + agent2 = register(container, TopologyAgent()) + agent3 = register(container, TopologyAgent()) + n1 = add_node!(topology, agent, agent2) + n3 = add_node!(topology, agent3) + add_edge!(topology, n1, n3) + end + + @test topology_neighbors(agent) == [address(agents(container)[3]), + address(agents(container)[2])] +end + @testset "TestModifyTopology" begin container = create_tcp_container("127.0.0.1", 3333) agent = nothing @@ -142,7 +159,7 @@ end topology = cycle_topology(4) container = create_tcp_container("127.0.0.1", 3333) - choose_agent(topology) do node + choose_agents!(topology) do node return register(container, TopologyAgent()) end @@ -160,7 +177,7 @@ end register(container, TopologyAgent()) register(container, TopologyAgent()) - assign_agent(topology, container) do agent, node + assign_agents!(topology, container) do agent, node return aid(agent) == "agent" * string(node.id - 1) end @@ -206,4 +223,96 @@ end end @test length(topology_neighbors(container["agent0"])) == 2 -end \ No newline at end of file +end + +@testset "TestTopologyGraphAPI" begin + n_nodes = 5 + topology = complete_topology(n_nodes) + @test length(collect(edges(topology))) == (n_nodes^2 - n_nodes) / 2 + @test collect(edges(topology))[1] ∈ collect(edges(topology)) + @test edgetype(topology) == Graphs.SimpleGraphs.SimpleEdge{Int64} + @test has_edge(topology, 1, 2) + @test has_vertex(topology, 1) + @test inneighbors(topology, 2) == [1, 3, 4, 5] + @test outneighbors(topology, 2) == [1, 3, 4, 5] + @test !is_directed(topology) + @test ne(topology) == (n_nodes^2 - n_nodes) / 2 + @test nv(topology) == 5 + @test collect(vertices(topology)) == [1, 2, 3, 4, 5] +end + +@testset "TestMarkForConnector" begin + topologyA = complete_topology(3, tid=:A) + topologyB = cycle_topology(3, tid=:B) + connect_topologies!(topologyA, topologyB) + + marked_A = TopologyAgent() + tr = TopologyRole() + add(marked_A, tr) + mark_as_connector!(marked_A) + marked_B = TopologyAgent() + mark_as_connector!(marked_B) + + agents_1 = [marked_A, TopologyAgent(), TopologyAgent()] + agents_2 = [marked_B, TopologyAgent(), TopologyAgent()] + + auto_assign!(topologyA, agents_1) + auto_assign!(topologyB, agents_2) + + @test length(topology_neighbors(marked_A, tid=:A)) == 2 + @test length(topology_connectors(marked_A, tid=:A)) == 1 + @test length(topology_connection_types(marked_A, tid=:A)) == 1 + @test length(topology_neighbors(tr, tid=:A)) == 2 + @test length(topology_connectors(tr, tid=:A)) == 1 + @test length(topology_connection_types(tr, tid=:A)) == 1 + + @test_throws ArgumentError topology_neighbors(tr, tid=:B) + @test_throws ArgumentError topology_connectors(tr, tid=:B) + @test_throws ArgumentError topology_connection_types(tr, tid=:B) +end + +@testset "TestSetAsConnector" begin + topologyA = complete_topology(3, tid=:A) + topologyB = cycle_topology(3, tid=:B) + connect_topologies!(topologyA, topologyB) + + marked_A = TopologyAgent() + set_as_connector!(topologyA, marked_A) + marked_B = TopologyAgent() + set_as_connector!(topologyB, marked_A) + + agents_1 = [marked_A, TopologyAgent(), TopologyAgent()] + agents_2 = [marked_B, TopologyAgent(), TopologyAgent()] + + auto_assign!(topologyA, agents_1) + auto_assign!(topologyB, agents_2) + + @test length(topology_neighbors(marked_A, tid=:A)) == 2 + @test length(topology_connectors(marked_A, tid=:A)) == 1 + @test length(topology_connection_types(marked_A, tid=:A)) == 1 +end + +@testset "TestAgentCharacteristicSymbol" begin + container = create_tcp_container("127.0.0.1", 3333) + agent = nothing + agent2 = nothing + + create_topology() do topology + agent = register(container, TopologyAgent()) + agent2 = register(container, TopologyAgent()) + agent3 = register(container, TopologyAgent()) + n1 = add_node!(topology, agent) + n2 = add_node!(topology, agent2) + n3 = add_node!(topology, agent3) + add_edge!(topology, n2, n1) + add_edge!(topology, n2, n3) + set_characteristic!(topology, n1, agent, :lead) + end + + @test topology_characteristic(agent) == :lead + @test length(topology_neighbors(agent, has_characteristic=:lead)) == 0 + @test length(topology_neighbors(agent2, has_characteristic=:lead)) == 1 + @test length(topology_neighbors(agent2)) == 2 + @test length(topology_neighbors(agent2, has_characteristic=[:lead])) == 1 + @test topology_node_id(agent) == 1 +end diff --git a/test/visualization_tests.jl b/test/visualization_tests.jl new file mode 100644 index 00000000..4d1436a1 --- /dev/null +++ b/test/visualization_tests.jl @@ -0,0 +1,114 @@ +using Mango +using Test +using Graphs +using Dates +using Makie +using GraphMakie +using CairoMakie + +@agent struct TopologyPlotAgent +end + +@agent struct MyVisuBehavingAgent + counter::Int + other_aid::String +end + +function Mango.on_step(agent::MyVisuBehavingAgent, environment::DefaultEnvironment, clock::Clock, step_size_s::Real) + if agent.counter > 10 + return + end + agent.counter += 1 + send_message(agent, "Trigger", AgentAddress(aid=agent.other_aid)) +end + +@testset "TestVisuAgents" begin + world = create_world(DateTime(Millisecond(0)), + communication_sim=SimpleCommunicationSimulation(default_delay_s=1)) + + agent1 = register(world, MyVisuBehavingAgent(0, "2"), "1") + agent2 = register(world, MyVisuBehavingAgent(0, "1"), "2") + + record_agent!((agent) -> agent.counter, world, "counter") + record_world!(() -> Second(world.clock.simulation_time).value, world, "time") + + activate(world) do + results = discrete_step_until(world, 1000) + end + + plot_world(world, "time", write_to="test_world_plot.svg") + plot_agents(world, "counter", write_to="test_agents_plot.svg") + plot_recordings(world, size=(800, 800), write_to="test_recordings_plot.svg") + + open("test_world_plot.svg", "r") do f + @test length(read(f, String)) > 1000 + end + rm("test_world_plot.svg") + + open("test_agents_plot.svg", "r") do f + @test length(read(f, String)) > 1000 + end + rm("test_agents_plot.svg") + + open("test_recordings_plot.svg", "r") do f + @test length(read(f, String)) > 1000 + end + rm("test_recordings_plot.svg") +end + +@testset "TestVisuAgentsComm" begin + world = create_world(DateTime(Millisecond(0)), + communication_sim=SimpleCommunicationSimulation(default_delay_s=1)) + + agent1 = register(world, MyVisuBehavingAgent(0, "2"), "1") + agent2 = register(world, MyVisuBehavingAgent(0, "1"), "2") + + topology = complete_topology(2) + auto_assign!(topology, world) + + activate(world) do + results = discrete_step_until(world, 1000) + end + + show_communication_data(world, show=false, based_on=topology) + rm("communication.svg") +end + +@testset "TestVisuAgentsTopo" begin + world = create_world(DateTime(Millisecond(0)), + communication_sim=SimpleCommunicationSimulation(default_delay_s=1)) + + agent1 = register(world, MyVisuBehavingAgent(0, "2"), "1") + agent2 = register(world, MyVisuBehavingAgent(0, "1"), "2") + + topology = complete_topology(3) + auto_assign!(topology, world) + + plot_node_topology(topology, write_to="test_topology_plot.svg") + rm("test_topology_plot.svg") +end + + +@testset "TestMultiTopo" begin + world = create_world(DateTime(Millisecond(0)), + communication_sim=SimpleCommunicationSimulation(default_delay_s=1)) + + agent1 = register(world, MyVisuBehavingAgent(0, "2"), "1") + agent2 = register(world, MyVisuBehavingAgent(0, "1"), "2") + agent3 = register(world, MyVisuBehavingAgent(0, "3"), "3") + mark_as_connector!(agent1) + agent4 = register(world, MyVisuBehavingAgent(0, "4"), "4") + agent5 = register(world, MyVisuBehavingAgent(0, "5"), "5") + agent6 = register(world, MyVisuBehavingAgent(0, "6"), "6") + mark_as_connector!(agent6) + + topology = complete_topology(3) + topology2 = complete_topology(3) + auto_assign!(topology, world) + auto_assign!(topology2, world) + connect_topologies!(topology, topology2) + + plot_multi_agent_topology([topology, topology2], write_to="test_topology_plot.svg") + @test stat("test_topology_plot.svg").size > 10000 + rm("test_topology_plot.svg") +end \ No newline at end of file diff --git a/test/world_tests.jl b/test/world_tests.jl new file mode 100644 index 00000000..4dbf16d1 --- /dev/null +++ b/test/world_tests.jl @@ -0,0 +1,520 @@ +using Mango +using Test +using Logging +using Dates +using Random + +import Mango.handle_message + + +@agent struct SimAgent + counter::Int +end + +function handle_message(agent::SimAgent, message::Any, meta::AbstractDict) + agent.counter += 10 + if haskey(meta, "test") + agent.counter += 1 + end +end + +@testset "WorldKwargs" begin + + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) + agent1 = SimAgent(0) + agent2 = SimAgent(0) + register(world, agent1) + register(world, agent2) + + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1)), test=2) + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 11 +end + +@testset "WorldNoProtocolSpecificAddr" begin + + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) + + @test isnothing(protocol_addr(world)) +end + +@testset "WorldNoValidTargetCustomAid" begin + + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) + agent1 = SimAgent(0) + agent2 = SimAgent(0) + register(world, agent1) + register(world, agent2, "a1") + + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid="abc")) + + @test_logs (:warn, "The container has no agent with id: abc (from AgentAddress(nothing, nothing, nothing) with String)") min_level = Logging.Warn begin + stepping_result = step_simulation(world, 1) + end + + @test agent1.counter == 0 + @test agent2.counter == 0 + @test aid(agent2) == "a1" +end + +@testset "SimpleInternalSimulationWithoutDelayContainerTest" begin + + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) + agent1 = SimAgent(0) + agent2 = SimAgent(0) + register(world, agent1) + register(world, agent2) + + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1))) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=aid(agent2))) + + stepping_result = step_simulation(world, 1) + + + @test agent1.counter == 10 + @test agent2.counter == 10 + + shutdown(world.container) + @test world.container.shutdown +end + +@testset "SimpleInternalSimulationDelayGreaterStepSize" begin + + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) + agent1 = SimAgent(0) + agent2 = SimAgent(0) + register(world, agent1) + register(world, agent2) + + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1))) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=aid(agent2))) + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 0 + @test agent2.counter == 0 + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 10 + @test agent2.counter == 10 +end + +@testset "SimpleInternalSimulationDelayMixedGreaterStepSize" begin + + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) + agent1 = SimAgent(0) + agent2 = SimAgent(0) + register(world, agent1) + register(world, agent2) + + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1))) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=aid(agent2))) + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 0 + @test agent2.counter == 0 + + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1))) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=aid(agent2))) + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 10 + @test agent2.counter == 10 + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 20 + @test agent2.counter == 20 +end + +@testset "SimpleInternalSimulationLinkSpecificDelay" begin + + com_sim = SimpleCommunicationSimulation(default_delay_s=0) + world = create_world(DateTime(Millisecond(0)), communication_sim=com_sim) + agent1 = SimAgent(0) + agent2 = SimAgent(0) + register(world, agent1) + register(world, agent2) + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent1))] = 1 + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 + + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1))) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=aid(agent2))) + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 10 + @test agent2.counter == 0 + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 10 + @test agent2.counter == 10 + +end + +@agent struct SimSchedulingAgent + counter::Int + scheduled_counter::Int +end + +function handle_message(agent::SimSchedulingAgent, message::Any, meta::AbstractDict) + agent.counter += 1 +end + +@testset "SimulationWithSpecificDelaysAndScheduledTasks" begin + + com_sim = SimpleCommunicationSimulation(default_delay_s=0) + world = create_world(DateTime(0), communication_sim=com_sim) + agent1 = SimSchedulingAgent(0, 0) + agent2 = SimSchedulingAgent(0, 0) + register(world, agent1) + register(world, agent2) + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent1))] = 1 + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 + + schedule(agent1, PeriodicTaskData(0.1)) do + agent1.scheduled_counter += 1 + end + schedule(agent1, InstantTaskData()) do + agent1.scheduled_counter += 100 + end + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1))) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=aid(agent2))) + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 1 + @test agent1.scheduled_counter == 111 + @test agent2.counter == 0 + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 1 + @test agent1.scheduled_counter == 121 + @test agent2.counter == 1 +end + +@testset "SimulationWithSpecificDelaysAndScheduledTasksPeriodicTaskDelayGrDelay" begin + + com_sim = SimpleCommunicationSimulation(default_delay_s=0) + world = create_world(DateTime(0), communication_sim=com_sim) + agent1 = SimSchedulingAgent(0, 0) + agent2 = SimSchedulingAgent(0, 0) + register(world, agent1) + register(world, agent2) + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent1))] = 1 + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 + + schedule(agent1, PeriodicTaskData(3)) do + agent1.scheduled_counter += 1 + end + schedule(agent1, InstantTaskData()) do + agent1.scheduled_counter += 100 + end + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1))) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=aid(agent2))) + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 1 + @test agent1.scheduled_counter == 101 + @test agent2.counter == 0 + + stepping_result = step_simulation(world, 3) + + @test agent1.counter == 1 + @test agent1.scheduled_counter == 102 + @test agent2.counter == 1 +end + +@testset "SimulationWithSpecificDelaysAndScheduledTasksPeriodicTaskDelayGrDelayDiscreteUntil" begin + + com_sim = SimpleCommunicationSimulation(default_delay_s=0) + world = create_world(DateTime(0), communication_sim=com_sim) + agent1 = SimSchedulingAgent(0, 0) + agent2 = SimSchedulingAgent(0, 0) + register(world, agent1) + register(world, agent2) + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent1))] = 1 + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 + + schedule(agent1, PeriodicTaskData(3)) do + agent1.scheduled_counter += 1 + end + schedule(agent1, InstantTaskData()) do + agent1.scheduled_counter += 100 + end + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1))) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=aid(agent2))) + + discrete_step_until(world, 4) + + @test agent1.counter == 1 + @test agent1.scheduled_counter == 102 + @test agent2.counter == 1 +end + +@agent struct ComplexSimSchedulingAgent + counter::Int + scheduled_counter::Int +end + +function handle_message(agent::ComplexSimSchedulingAgent, message::Any, meta::AbstractDict) + agent.counter += 1 + schedule(agent, InstantTaskData()) do + agent.scheduled_counter += 100 + end +end + +@testset "SimulationWithSpecificDelaysAndScheduledTasksOnHandle" begin + + com_sim = SimpleCommunicationSimulation(default_delay_s=0) + world = create_world(DateTime(0), communication_sim=com_sim) + agent1 = ComplexSimSchedulingAgent(0, 0) + agent2 = ComplexSimSchedulingAgent(0, 0) + register(world, agent1) + register(world, agent2) + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent1))] = 1 + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 + + schedule(agent1, PeriodicTaskData(0.1)) do + agent1.scheduled_counter += 1 + end + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1))) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=aid(agent2))) + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 1 + @test agent1.scheduled_counter == 111 + @test agent2.counter == 0 + @test agent2.scheduled_counter == 0 + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 1 + @test agent1.scheduled_counter == 121 + @test agent2.counter == 1 + @test agent2.scheduled_counter == 100 +end + +@agent struct MoreComplexSimSchedulingAgent + counter::Int + scheduled_counter::Int +end + +function handle_message(agent::MoreComplexSimSchedulingAgent, message::Any, meta::AbstractDict) + agent.counter += 1 + schedule(agent, InstantTaskData()) do + agent.scheduled_counter += 100 + if message == "Hello Friends, this is RSc!" + reply_to(agent, "ABC", meta) + end + end +end + +@testset "SimulationWithSpecificDelaysWithReplyOnHandle" begin + + com_sim = SimpleCommunicationSimulation(default_delay_s=0) + world = create_world(DateTime(0), communication_sim=com_sim) + agent1 = MoreComplexSimSchedulingAgent(0, 0) + agent2 = MoreComplexSimSchedulingAgent(0, 0) + register(world, agent1) + register(world, agent2) + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent1))] = 1 + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 + + schedule(agent1, PeriodicTaskData(0.1)) do + agent1.scheduled_counter += 1 + end + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1)), aid(agent2)) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=aid(agent2))) + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 1 + @test agent1.scheduled_counter == 111 + @test agent2.counter == 1 + @test agent2.scheduled_counter == 100 + + stepping_result = step_simulation(world, 1) + + @test agent1.counter == 1 + @test agent1.scheduled_counter == 121 + @test agent2.counter == 2 + @test agent2.scheduled_counter == 200 +end + +@testset "SimulationWithSpecificDelaysWithReplyOnHandleDiscreteEvent" begin + + com_sim = SimpleCommunicationSimulation(default_delay_s=0) + world = create_world(DateTime(0), communication_sim=com_sim) + agent1 = MoreComplexSimSchedulingAgent(0, 0) + agent2 = MoreComplexSimSchedulingAgent(0, 0) + register(world, agent1) + register(world, agent2) + com_sim.delay_s_directed_edge_dict[(aid(agent2), aid(agent1))] = 1 + com_sim.delay_s_directed_edge_dict[(nothing, aid(agent2))] = 2 + + schedule(agent1, InstantTaskData()) do + agent1.scheduled_counter += 1 + end + schedule(agent1, InstantTaskData()) do + agent1.scheduled_counter += 1 + end + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1)), aid(agent2)) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=aid(agent2))) + + stepping_result = step_simulation(world) + + @test stepping_result.simulation_step_size_s == 0 + @test agent1.counter == 0 + @test agent1.scheduled_counter == 2 + @test agent2.counter == 0 + @test agent2.scheduled_counter == 0 + + stepping_result = step_simulation(world) + + @test stepping_result.simulation_step_size_s == 1 + @test agent1.counter == 1 + @test agent1.scheduled_counter == 102 + @test agent2.counter == 1 + @test agent2.scheduled_counter == 100 + + stepping_result = step_simulation(world) + + @test stepping_result.simulation_step_size_s == 1 + @test agent1.counter == 1 + @test agent1.scheduled_counter == 102 + @test agent2.counter == 2 + @test agent2.scheduled_counter == 200 + + stepping_result = step_simulation(world) + + @test isnothing(stepping_result) + + schedule(agent1, PeriodicTaskData(0.1)) do + agent1.scheduled_counter += 1 + end + schedule(agent1, PeriodicTaskData(3)) do + # nothing + end + + stepping_result = step_simulation(world) + + @test stepping_result.simulation_step_size_s == 0 + @test agent1.counter == 1 + @test agent1.scheduled_counter == 103 + @test agent2.counter == 2 + @test agent2.scheduled_counter == 200 + + stepping_result = step_simulation(world) + + @test stepping_result.simulation_step_size_s == 0.1 + @test agent1.counter == 1 + @test agent1.scheduled_counter == 104 + @test agent2.counter == 2 + @test agent2.scheduled_counter == 200 +end + +import Mango + +@testset "SimulationSchedulerDetermineError" begin + s = SimulationScheduler(clock=Clock(DateTime(0))) + push!(s.queue, Task("")) + + @test_throws "This should not happen! Did you schedule a task with zero sleep time?" Mango.determine_next_event_time_with(s, DateTime(0)) +end + +struct TestTaskSim <: TaskSimulation +end + +@testset "SimulationSchedulerDetermineNoImplent" begin + @test_throws "Please implement determine_next_event_time(...)" Mango.determine_next_event_time(TestTaskSim()) +end + +@testset "WorldAgentsAreOrdered" begin + world = create_world(DateTime(0)) + a1 = register(world, SimAgent(0)) + a2 = register(world, SimAgent(1)) + a3 = register(world, SimAgent(2)) + a4 = register(world, SimAgent(3)) + + @test agents(world)[1] == a1 + @test agents(world)[2] == a2 + @test agents(world)[3] == a3 + @test agents(world)[4] == a4 + @test world[aid(a1)] == a1 + @test world[1] == a1 +end + + +@agent struct ErrorAgent + counter::Int +end + +function handle_message(agent::ErrorAgent, message::Any, meta::AbstractDict) + throw("Something") +end + +@testset "WorldTestExceptionInScheduledTask" begin + world = create_world(DateTime(0)) + + activate(world) do + schedule(env(world), DelayTaskData(1)) do + throw("Noooo") + end + step_simulation(world) # start scheduling = 0 + @test_throws CompositeException step_simulation(world) # schedule task = 1 + end + +end + +@testset "WorldTestExceptionInMessageHandling" begin + world = create_world(DateTime(0)) + a1 = register(world, ErrorAgent(0)) + a2 = register(world, ErrorAgent(0)) + + activate(world) do + send_message(a1, "1", address(a2)) + @test_throws CompositeException step_simulation(world) # schedule task = 1 + end +end + +@testset "WorldWithPoisson" begin + Random.seed!(1) + + world = create_world(DateTime(0)) + + a1 = register(world, SimAgent(0)) + a2 = register(world, SimAgent(1)) + a3 = register(world, SimAgent(2)) + + topo = complete_topology(3) + auto_assign!(topo, world) + + aid_graph = topology_to_aid_graph(topo) + poisson_com_provider = create_distribution_based_com_sim(aid_graph, agents(world), base_delay_per_message_ms=15) + world.communication_sim = poisson_com_provider + + activate(world) do + send_message(a1, "Hello Friends, this is RSd!", AgentAddress(aid=aid(a2))) + send_message(a1, "Hello Friends, this is RSd!", AgentAddress(aid=aid(a3))) + + stepping_result = step_simulation(world) + + @test Mango.time(world) == DateTime("0000-01-01T00:01:40") + + send_message(a1, "Hello Friends, this is RSd!", AgentAddress(aid=aid(a3))) + + stepping_result = step_simulation(world) + + @test Mango.time(world) == DateTime("0000-01-01T00:03:20") + end +end \ No newline at end of file