From d60ccd7c800a6b63c67a686f1f246d169c0fa6a8 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Wed, 4 Sep 2024 21:01:28 +0200 Subject: [PATCH 01/54] Add express example to readme. --- README.md | 113 +++++++++++++++++++++++++++++------------------------- 1 file changed, 61 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 19eada0f..58ed02e2 100644 --- a/README.md +++ b/README.md @@ -67,60 +67,69 @@ 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 -using Mango - -# Create the container instances with TCP protocol -container = create_tcp_container("127.0.0.1", 5555) -container2 = create_tcp_container("127.0.0.1", 5556) - -# An agent in `Mango.jl` is a struct defined with the `@agent` macro. -# We define a `TCPPingPongAgent` that has an internal counter for incoming messages. -@agent struct TCPPingPongAgent - counter::Int -end - -# Create instances of ping pong agents -ping_agent = TCPPingPongAgent(0) -pong_agent = TCPPingPongAgent(0) - -# register each agent to a container and give them a name -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. -# Using Julias multiple dispatch, we can define a new `handle_message` method for our agent. -function Mango.handle_message(agent::TCPPingPongAgent, message::Any, meta::Any) - agent.counter += 1 - - println( - "$(agent.aid) got a message: $message." * - "This is message number: $(agent.counter) for me!" - ) - - # doing very important work - sleep(0.5) - - if message == "Ping" - reply_to(agent, "Pong", meta) - elseif message == "Pong" - reply_to(agent, "Ping", meta) +
+ With Express API + ```julia + ``` +
+ +
+ With Container Creation + ```julia + using Mango + + # Create the container instances with TCP protocol + container = create_tcp_container("127.0.0.1", 5555) + container2 = create_tcp_container("127.0.0.1", 5556) + + # An agent in `Mango.jl` is a struct defined with the `@agent` macro. + # We define a `TCPPingPongAgent` that has an internal counter for incoming messages. + @agent struct TCPPingPongAgent + counter::Int end -end - -# With all this in place, we can send a message to the first agent to start the repeated message exchange. -# To do this, we need to start the containers so they listen to incoming messages and send the initating message. -# The best way to start the container message loops and ensure they are correctly shut down in the end is the -# `activate(containers)` function. -activate([container, container2]) do - send_message(ping_agent, "Ping", address(pong_agent)) - - # wait for 5 messages to have been sent - while ping_agent.counter < 5 - sleep(1) + + # Create instances of ping pong agents + ping_agent = TCPPingPongAgent(0) + pong_agent = TCPPingPongAgent(0) + + # register each agent to a container and give them a name + 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. + # Using Julias multiple dispatch, we can define a new `handle_message` method for our agent. + function Mango.handle_message(agent::TCPPingPongAgent, message::Any, meta::Any) + agent.counter += 1 + + println( + "$(agent.aid) got a message: $message." * + "This is message number: $(agent.counter) for me!" + ) + + # doing very important work + sleep(0.5) + + if message == "Ping" + reply_to(agent, "Pong", meta) + elseif message == "Pong" + reply_to(agent, "Ping", meta) + end end -end -``` + + # With all this in place, we can send a message to the first agent to start the repeated message exchange. + # To do this, we need to start the containers so they listen to incoming messages and send the initating message. + # The best way to start the container message loops and ensure they are correctly shut down in the end is the + # `activate(containers)` function. + activate([container, container2]) do + send_message(ping_agent, "Ping", address(pong_agent)) + + # wait for 5 messages to have been sent + while ping_agent.counter < 5 + sleep(1) + end + end + ``` +
## License Mango.jl is developed and published under the MIT license. From 27085cadd2c24863f6a5b64577feaf8cef953e8c Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Wed, 4 Sep 2024 21:04:00 +0200 Subject: [PATCH 02/54] Add express example to readme. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 58ed02e2..1199aa15 100644 --- a/README.md +++ b/README.md @@ -69,12 +69,15 @@ The following simple showcase demonstrates how you can define agents in Mango. J
With Express API + ```julia + asd ```
With Container Creation + ```julia using Mango From 5c5da1f483f538c61cd2a0de9b23d819ad3ab9db Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Wed, 4 Sep 2024 21:12:40 +0200 Subject: [PATCH 03/54] Adjust readme with express example. --- README.md | 134 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 81 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 1199aa15..6c9b562b 100644 --- a/README.md +++ b/README.md @@ -68,70 +68,98 @@ 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)!
- With Express API - - ```julia - asd - ``` + With Container Creation + + ```julia +using Mango + +# Create the container instances with TCP protocol +container = create_tcp_container("127.0.0.1", 5555) +container2 = create_tcp_container("127.0.0.1", 5556) + +# An agent in `Mango.jl` is a struct defined with the `@agent` macro. +# We define a `TCPPingPongAgent` that has an internal counter for incoming messages. +@agent struct TCPPingPongAgent + counter::Int +end + +# Create instances of ping pong agents +ping_agent = TCPPingPongAgent(0) +pong_agent = TCPPingPongAgent(0) + +# register each agent to a container and give them a name +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. +# Using Julias multiple dispatch, we can define a new `handle_message` method for our agent. +function Mango.handle_message(agent::TCPPingPongAgent, message::Any, meta::Any) + agent.counter += 1 + + println( + "$(agent.aid) got a message: $message." * + "This is message number: $(agent.counter) for me!" + ) + + # doing very important work + sleep(0.5) + + if message == "Ping" + reply_to(agent, "Pong", meta) + elseif message == "Pong" + reply_to(agent, "Ping", meta) + end +end + +# With all this in place, we can send a message to the first agent to start the repeated message exchange. +# To do this, we need to start the containers so they listen to incoming messages and send the initating message. +# The best way to start the container message loops and ensure they are correctly shut down in the end is the +# `activate(containers)` function. +activate([container, container2]) do + send_message(ping_agent, "Ping", address(pong_agent)) + + # wait for 5 messages to have been sent + while ping_agent.counter < 5 + sleep(1) + end +end + ```
-
- With Container Creation - - ```julia - using Mango - - # Create the container instances with TCP protocol - container = create_tcp_container("127.0.0.1", 5555) - container2 = create_tcp_container("127.0.0.1", 5556) +In newer versions of Mango.jl, the express API is introduced, which rewrites the code above to: - # An agent in `Mango.jl` is a struct defined with the `@agent` macro. - # We define a `TCPPingPongAgent` that has an internal counter for incoming messages. - @agent struct TCPPingPongAgent - counter::Int - end +
+ With Express API - # Create instances of ping pong agents - ping_agent = TCPPingPongAgent(0) - pong_agent = TCPPingPongAgent(0) + ```julia +using Mango - # register each agent to a container and give them a name - register(container, ping_agent, "Agent_1") - register(container2, pong_agent, "Agent_2") +@agent struct TCPPingPongAgent + counter::Int +end - # When an incoming message is addressed at an agent, its container will call the `handle_message` function for it. - # Using Julias multiple dispatch, we can define a new `handle_message` method for our agent. - function Mango.handle_message(agent::TCPPingPongAgent, message::Any, meta::Any) - agent.counter += 1 +function Mango.handle_message(agent::TCPPingPongAgent, message::Any, meta::Any) + agent.counter += 1 - println( - "$(agent.aid) got a message: $message." * - "This is message number: $(agent.counter) for me!" - ) + println( + "$(agent.aid) got a message: $message." * + "This is message number: $(agent.counter) for me!" + ) - # doing very important work - sleep(0.5) + sleep(0.5) - if message == "Ping" - reply_to(agent, "Pong", meta) - elseif message == "Pong" - reply_to(agent, "Ping", meta) - end + if message == "Ping" + reply_to(agent, "Pong", meta) + elseif message == "Pong" + reply_to(agent, "Ping", meta) end +end - # With all this in place, we can send a message to the first agent to start the repeated message exchange. - # To do this, we need to start the containers so they listen to incoming messages and send the initating message. - # The best way to start the container message loops and ensure they are correctly shut down in the end is the - # `activate(containers)` function. - activate([container, container2]) do - send_message(ping_agent, "Ping", address(pong_agent)) - - # wait for 5 messages to have been sent - while ping_agent.counter < 5 - sleep(1) - end - end - ``` +run_with_tcp(2, (TCPPingPongAgent(0), :aid => "Agent_1"), (TCPPingPongAgent(0), :aid => "Agent_2")) do + send_message(ping_agent, "Ping", address(pong_agent)) + sleep_until(ping_agent.counter < 5) +end + ```
## License From adfc92d07cad2c66cd0624384966ad7c982f777a Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Wed, 4 Sep 2024 21:19:42 +0200 Subject: [PATCH 04/54] Fixing express example. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c9b562b..408ee73c 100644 --- a/README.md +++ b/README.md @@ -155,9 +155,11 @@ function Mango.handle_message(agent::TCPPingPongAgent, message::Any, meta::Any) end end -run_with_tcp(2, (TCPPingPongAgent(0), :aid => "Agent_1"), (TCPPingPongAgent(0), :aid => "Agent_2")) do +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) + sleep_until(() -> ping_agent.counter >= 5) end ```
From e1b25e48fc5969f606ebeb2ffad38e20686aeb89 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Sun, 8 Sep 2024 18:28:38 +0200 Subject: [PATCH 05/54] Add global events. --- src/agent/core.jl | 28 ++++++++++++++++++++- src/agent/role.jl | 12 ++++++++- src/container/simulation.jl | 49 +++++++++++++++++++++++++++++++++++-- src/world/core.jl | 24 ++++++++++++++++++ 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/src/agent/core.jl b/src/agent/core.jl index 8f6660e5..bfef9180 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -15,7 +15,8 @@ export @agent, ForwardingRule, service_of_type, add_service!, - services + services, + on_global_event using UUIDs @@ -522,4 +523,29 @@ 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::DataType) 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 end \ No newline at end of file diff --git a/src/agent/role.jl b/src/agent/role.jl index abfaf1b3..f31b0cc6 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 """ @@ -335,3 +336,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 \ No newline at end of file diff --git a/src/container/simulation.jl b/src/container/simulation.jl index b1aaca27..01cc4fc5 100644 --- a/src/container/simulation.jl +++ b/src/container/simulation.jl @@ -1,4 +1,4 @@ -export SimulationContainer, register, send_message, shutdown, protocol_addr, create_simulation_container, step_simulation, SimulationResult, CommunicationSimulationResult, TaskSimulationResult, on_step +export SimulationContainer, register, send_message, shutdown, protocol_addr, create_simulation_container, step_simulation, SimulationResult, CommunicationSimulationResult, TaskSimulationResult, on_step, discrete_event_simulation using Base.Threads using Dates @@ -27,7 +27,7 @@ Per default the [`SimpleCommunicationSimulation`](@ref) is used for communicatio [`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) +function create_simulation_container(start_time::DateTime; communication_sim::Union{Nothing,CommunicationSimulation}=nothing, task_sim::Union{Nothing,TaskSimulation}=nothing, space::Space=nothing) container = SimulationContainer() container.clock.simulation_time = start_time if !isnothing(communication_sim) @@ -36,6 +36,10 @@ function create_simulation_container(start_time::DateTime; communication_sim::Un if !isnothing(task_sim) container.task_sim = task_sim end + if !isnothing(space) + container.world = World(space=space) + end + add_observer(container.world, container.world_observer) return container end @@ -48,6 +52,16 @@ struct MessageData arriving_time::DateTime end +struct DispatchToAgentWorldObserver <: WorldObserver + agents_ref::Dict +end + +function dispatch_global_event(observer::DispatchToAgentWorldObserver, event::Any) + for agent in values(observer.agents_ref) + dispatch_global_event(agent, event) + end +end + """ The SimulationContainer used as a base struct to enable simulations in Mango.jl. Shall be created using [`create_simulation_container`](@ref). @@ -61,6 +75,7 @@ using [`create_simulation_container`](@ref). shutdown::Bool = false communication_sim::CommunicationSimulation = SimpleCommunicationSimulation() message_queue::ConcurrentQueue{MessageData} = ConcurrentQueue{MessageData}() + world_observer::WorldObserver = DispatchToAgentWorldObserver(agents) end function agents(container::SimulationContainer)::Vector{Agent} @@ -84,6 +99,10 @@ function on_step(role::Role, world::World, clock::Clock, step_size_s::Real) # default nothing end +function on_step(space::Space, world::World, clock::Clock, step_size_s::Real) + # default nothing +end + """ Internal, call on_step on all agents. """ @@ -279,6 +298,8 @@ function step_simulation(container::SimulationContainer, step_size_s::Real=DISCR @debug "Finish simulation iteration" state_changed end + on_step(container.world.space, container.world, container.clock, time_step_s) + # agents act on the stepping hook for agent in values(container.agents) step_agent(agent, container.world, container.clock, time_step_s) @@ -293,6 +314,30 @@ function step_simulation(container::SimulationContainer, step_size_s::Real=DISCR return SimulationResult(elapsed, messaging_sim_result, task_sim_result, time_step_s) end +""" + discrete_event_simulation(container::SimulationContainer, max_advance_time_s::Real) + +Execute a discrete event simulation using the `container` with the maximal allowed advanced time +of the simulation of `max_advance_time_s`. + +This function will step the container until the clock has advanced to the initial_time + `max_advance_time_s` +or if the time of the container does not advance anymore (which would mean no events are scheduled). +""" +function discrete_event_simulation(container::SimulationContainer, max_advance_time_s::Real) + initial_time = container.clock.simulation_time + prev_time = nothing + results = [] + + while isnothing(prev_time) || (prev_time < container.clock.simulation_time + && + initial_time + Second(max_advance_time_s) <= container.clock.simulation_time) + + prev_time = container.clock.simulation_time + push!(results, step_simulation(container)) + end + return results +end + function protocol_addr(container::SimulationContainer) return nothing end diff --git a/src/world/core.jl b/src/world/core.jl index d6579420..93a69dba 100644 --- a/src/world/core.jl +++ b/src/world/core.jl @@ -2,6 +2,11 @@ export World, Space, Position, Position2D, Area2D, location, move, initialize, i abstract type Position end abstract type Space{P<:Position} end +abstract type WorldObserver end + +function dispatch_global_event(observer::WorldObserver, event::Any) + # default no reaction +end struct Position2D <: Position x::Real @@ -16,6 +21,7 @@ end @kwdef struct World{S<:Space} space::S = Area2D(width=10, height=10) + observers::Vector{WorldObserver} = Vector() initialized::Bool = false end @@ -52,3 +58,21 @@ end function initialized(world::World) return world.initialized end + +function add_observer(world::World, observer::Any) + push!(world.observers, observer) +end + +""" + emit_global_event(world::World, event::Any) + +Emit a global world event. This types of events can be handled by any agent +living in the world (resp. living in the container, the world 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(world::World, event::Any) + for observer in world.observers + dispatch_global_event(observer, event) + end +end From e1dd5756f58d6be7b43437ad38ca4d87d9d950af Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Mon, 9 Sep 2024 14:46:05 +0200 Subject: [PATCH 06/54] Adding Environment, world has scheduler now. --- .github/workflows/test-mango.yml | 8 +- src/container/simulation.jl | 14 ++-- src/simulation/tasks.jl | 121 +++---------------------------- src/util/scheduling.jl | 110 ++++++++++++++++++++++++++++ src/world/core.jl | 26 ++++++- 5 files changed, 157 insertions(+), 122 deletions(-) diff --git a/.github/workflows/test-mango.yml b/.github/workflows/test-mango.yml index 34919006..34d5534e 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, reopened] # needed to allow julia-actions/cache to delete caches diff --git a/src/container/simulation.jl b/src/container/simulation.jl index 01cc4fc5..643178f3 100644 --- a/src/container/simulation.jl +++ b/src/container/simulation.jl @@ -1,4 +1,6 @@ -export SimulationContainer, register, send_message, shutdown, protocol_addr, create_simulation_container, step_simulation, SimulationResult, CommunicationSimulationResult, TaskSimulationResult, on_step, discrete_event_simulation +export SimulationContainer, register, send_message, shutdown, protocol_addr, + create_simulation_container, step_simulation, SimulationResult, CommunicationSimulationResult, + TaskSimulationResult, on_step, discrete_event_simulation using Base.Threads using Dates @@ -39,7 +41,8 @@ function create_simulation_container(start_time::DateTime; communication_sim::Un if !isnothing(space) container.world = World(space=space) end - add_observer(container.world, container.world_observer) + add_observer!(container.world, container.world_observer) + add_simulation_scheduler!(container.task_sim, container.world.scheduler) return container end @@ -63,8 +66,7 @@ function dispatch_global_event(observer::DispatchToAgentWorldObserver, event::An end """ -The SimulationContainer used as a base struct to enable simulations in Mango.jl. Shall be created -using [`create_simulation_container`](@ref). +The SimulationContainer used as a base struct to enable simulations in Mango.jl. Always create using [`create_simulation_container`](@ref). """ @kwdef mutable struct SimulationContainer <: ContainerInterface world::World = World() @@ -99,10 +101,6 @@ function on_step(role::Role, world::World, clock::Clock, step_size_s::Real) # default nothing end -function on_step(space::Space, world::World, clock::Clock, step_size_s::Real) - # default nothing -end - """ Internal, call on_step on all agents. """ diff --git a/src/simulation/tasks.jl b/src/simulation/tasks.jl index 434f6308..eee25be9 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 @@ -202,14 +99,14 @@ function step_iteration(task_sim::SimpleTaskSimulation, step_size_s::Real, first # 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 diff --git a/src/util/scheduling.jl b/src/util/scheduling.jl index 35ad44d0..b910bb42 100644 --- a/src/util/scheduling.jl +++ b/src/util/scheduling.jl @@ -295,4 +295,114 @@ 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, 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 \ No newline at end of file diff --git a/src/world/core.jl b/src/world/core.jl index 93a69dba..77ce3709 100644 --- a/src/world/core.jl +++ b/src/world/core.jl @@ -3,11 +3,14 @@ export World, Space, Position, Position2D, Area2D, location, move, initialize, i abstract type Position end abstract type Space{P<:Position} end abstract type WorldObserver end +abstract type Environment end function dispatch_global_event(observer::WorldObserver, event::Any) # default no reaction end +struct NoEnv end + struct Position2D <: Position x::Real y::Real @@ -21,10 +24,27 @@ end @kwdef struct World{S<:Space} space::S = Area2D(width=10, height=10) + environment::Environment = NoEnv() + scheduler::AbstractScheduler = SimulationScheduler() observers::Vector{WorldObserver} = Vector() initialized::Bool = false end +schedule(f::Function, world::World, data::TaskData) = schedule(f, world.scheduler, data) + +function on_step(world::World, clock::Clock, step_size_s::Real) + # default do nothing +end + +function on_step(environment::Environment, world::World, clock::Clock, step_size_s::Real) + # default do nothing +end + +function step(world::World, clock::Clock, step_size_s::Real) + on_step(world, clock, step_size_s) + on_step(environment, world, clock, step_size_s) +end + function location(space::Space{P}, agent::Agent)::P where {P<:Position} throw("Position on the space $space not defined!") end @@ -59,10 +79,14 @@ function initialized(world::World) return world.initialized end -function add_observer(world::World, observer::Any) +function add_observer!(world::World, observer::Any) push!(world.observers, observer) end +function environment(world::World) + return world.environment +end + """ emit_global_event(world::World, event::Any) From 81d6a721452e78bcdfaa38b277e77f8a14fd01bb Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Mon, 9 Sep 2024 14:49:50 +0200 Subject: [PATCH 07/54] Adding Environment, world has scheduler now. --- .github/workflows/test-mango.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-mango.yml b/.github/workflows/test-mango.yml index 34d5534e..37a658e7 100644 --- a/.github/workflows/test-mango.yml +++ b/.github/workflows/test-mango.yml @@ -6,7 +6,7 @@ on: - main - development pull_request: - types: [opened, reopened] + types: [opened, synchronize, reopened] # needed to allow julia-actions/cache to delete caches From 1e6ade2676df53fe805f7d261e253e8c8b4b1cb9 Mon Sep 17 00:00:00 2001 From: Mehmet Hakan Satman Date: Tue, 1 Oct 2024 11:03:55 +0300 Subject: [PATCH 08/54] update bibtex --- paper/paper.bib | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paper/paper.bib b/paper/paper.bib index 1bd52bd9..cf34618d 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}, @@ -127,7 +127,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", From 4d766d9cf9239388b12be0c91fea6f27b6f25b7e Mon Sep 17 00:00:00 2001 From: Mehmet Hakan Satman Date: Tue, 1 Oct 2024 11:16:47 +0300 Subject: [PATCH 09/54] update paper --- paper/paper.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/paper/paper.md b/paper/paper.md index 533e07f5..63978a59 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -58,7 +58,7 @@ 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. +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. @@ -82,11 +82,11 @@ 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. @@ -95,8 +95,6 @@ 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. Containers can be set up and equipped with the necessary TCP protocol. From 3a6da6b5d49d7cd4abe8f1d66b3a35e0c155fe02 Mon Sep 17 00:00:00 2001 From: "Daniel S. Katz" Date: Tue, 1 Oct 2024 08:19:26 -0500 Subject: [PATCH 10/54] small changes in paper.md --- paper/paper.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/paper/paper.md b/paper/paper.md index 63978a59..a3ffd8dc 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. @@ -59,7 +59,7 @@ While `Mango.jl` is a general purpose multi-agent framework, we will focus on en 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. +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. @@ -89,14 +89,14 @@ 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 -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 @@ -129,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 From d7acac2451c3c270af97ad1861a774b1be01287c Mon Sep 17 00:00:00 2001 From: "Daniel S. Katz" Date: Tue, 1 Oct 2024 08:21:49 -0500 Subject: [PATCH 11/54] minor changes in paper.bib --- paper/paper.bib | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/paper/paper.bib b/paper/paper.bib index cf34618d..57d8fe6b 100644 --- a/paper/paper.bib +++ b/paper/paper.bib @@ -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, @@ -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 +} From 5eca62dbb45ab2fc663181954c0d0ce4a5c1fc1a Mon Sep 17 00:00:00 2001 From: jsagerOffis <150672119+jsagerOffis@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:36:04 +0200 Subject: [PATCH 12/54] Create CITATION.cff --- CITATION.cff | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 CITATION.cff 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" From e3ef6081a81a09a409eacaffde48a8c39cda2317 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 1 Oct 2024 16:39:52 +0200 Subject: [PATCH 13/54] Update README.md Add joss link. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 19eada0f..d89e2dd8 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ ![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) From b4d400b32dc7f1d9caf68dcb6301f4005798fe2c Mon Sep 17 00:00:00 2001 From: Jens Sager Date: Fri, 4 Oct 2024 14:31:48 +0200 Subject: [PATCH 14/54] add graphs api forwarding to topology --- src/world/topology.jl | 41 +++++++++++++++++++++++++++++++++++++++++ test/topology_tests.jl | 16 ++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/world/topology.jl b/src/world/topology.jl index 3511b56b..d760aab2 100644 --- a/src/world/topology.jl +++ b/src/world/topology.jl @@ -272,4 +272,45 @@ end function topology_neighbors(role::Role, state::State=NORMAL)::Vector{AgentAddress} return neighbors(service_of_type(role.context.agent, TopologyService, TopologyService()), state) +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 \ No newline at end of file diff --git a/test/topology_tests.jl b/test/topology_tests.jl index d631a11b..048de7bc 100644 --- a/test/topology_tests.jl +++ b/test/topology_tests.jl @@ -206,4 +206,20 @@ end end @test length(topology_neighbors(container["agent0"])) == 2 +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 \ No newline at end of file From 7eef7f8ca1652b0b1abce1a95b6451ab26425f0c Mon Sep 17 00:00:00 2001 From: Jens Sager Date: Fri, 4 Oct 2024 14:53:10 +0200 Subject: [PATCH 15/54] add part in documentation for graphs API --- docs/src/topology.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/src/topology.md b/docs/src/topology.md index 7c1f86c1..a171d28b 100644 --- a/docs/src/topology.md +++ b/docs/src/topology.md @@ -49,6 +49,24 @@ end topology_neighbors(container[1]) ``` +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`. From eae814e0c904ebaf40919c096da07f1267fe981a Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Fri, 1 Nov 2024 13:02:54 +0100 Subject: [PATCH 16/54] Several world bugfixes. Added recorder. --- README.md | 20 ----------- src/container/simulation.jl | 70 +++++++++++++++++++++++++----------- src/util/scheduling.jl | 4 +-- src/world/core.jl | 72 ++++++++++++++++++++++++++++++++----- 4 files changed, 115 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 408ee73c..884e5f76 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,11 @@ [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) [![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. @@ -166,13 +156,3 @@ end ## License Mango.jl is developed and published under the MIT license. - - - - - - - diff --git a/src/container/simulation.jl b/src/container/simulation.jl index 643178f3..c339fbde 100644 --- a/src/container/simulation.jl +++ b/src/container/simulation.jl @@ -1,6 +1,6 @@ export SimulationContainer, register, send_message, shutdown, protocol_addr, create_simulation_container, step_simulation, SimulationResult, CommunicationSimulationResult, - TaskSimulationResult, on_step, discrete_event_simulation + TaskSimulationResult, on_step, discrete_step_until, env, space, world, time, clock using Base.Threads using Dates @@ -29,7 +29,12 @@ Per default the [`SimpleCommunicationSimulation`](@ref) is used for communicatio [`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, space::Space=nothing) +function create_simulation_container(start_time::DateTime; + communication_sim::Union{Nothing,CommunicationSimulation}=nothing, + task_sim::Union{Nothing,TaskSimulation}=nothing, + space::Union{Nothing,Space}=nothing, + env::Union{Nothing,Environment}=nothing) + container = SimulationContainer() container.clock.simulation_time = start_time if !isnothing(communication_sim) @@ -39,7 +44,10 @@ function create_simulation_container(start_time::DateTime; communication_sim::Un container.task_sim = task_sim end if !isnothing(space) - container.world = World(space=space) + container.world.space = space + end + if !isnothing(env) + container.world.environment = env end add_observer!(container.world, container.world_observer) add_simulation_scheduler!(container.task_sim, container.world.scheduler) @@ -56,7 +64,7 @@ struct MessageData end struct DispatchToAgentWorldObserver <: WorldObserver - agents_ref::Dict + agents_ref::OrderedDict{String,Agent} end function dispatch_global_event(observer::DispatchToAgentWorldObserver, event::Any) @@ -69,10 +77,10 @@ end The SimulationContainer used as a base struct to enable simulations in Mango.jl. Always create using [`create_simulation_container`](@ref). """ @kwdef mutable struct SimulationContainer <: ContainerInterface - world::World = World() clock::Clock = Clock(DateTime(0)) + world::World = World(scheduler=SimulationScheduler(clock=clock)) task_sim::TaskSimulation = SimpleTaskSimulation(clock=clock) - agents::OrderedDict{String,Agent} = OrderedDict() + agents::OrderedDict{String,Agent} = OrderedDict{String,Agent}() agent_counter::Integer = 0 shutdown::Bool = false communication_sim::CommunicationSimulation = SimpleCommunicationSimulation() @@ -193,13 +201,13 @@ function cs_step_iteration(container::SimulationContainer, communication_result = pre_communication_result if isnothing(communication_result) communication_result = calculate_communication(container.communication_sim, - container.clock, + clock(container), 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 + if add_seconds(mp.sent_date, pr.delay_s) <= add_seconds(time(container), step_size_s) && pr.reached state_changed = true @spawnlog process_message(container, mp.content[1], mp.content[2]) else @@ -216,13 +224,13 @@ 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) + communication_result = calculate_communication(container.communication_sim, clock(container), 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 + time_to_next_message_s = (findmin(message_arrival_times)[1] - time(container)).value / 1000 end @debug "Next message in $time_to_next_message_s" @@ -260,7 +268,7 @@ function step_simulation(container::SimulationContainer, step_size_s::Real=DISCR state_changed = true - @debug "Time" container.clock + @debug "Time at the start of the step" time(container) task_sim_result = TaskSimulationResult() messaging_sim_result = MessagingSimulationResult() @@ -296,18 +304,18 @@ function step_simulation(container::SimulationContainer, step_size_s::Real=DISCR @debug "Finish simulation iteration" state_changed end - on_step(container.world.space, container.world, container.clock, time_step_s) + step(container.world, clock(container), time_step_s) # agents act on the stepping hook for agent in values(container.agents) - step_agent(agent, container.world, container.clock, time_step_s) + step_agent(agent, container.world, clock(container), 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) + container.clock.simulation_time = add_seconds(time(container), time_step_s) - @debug "new time", container.clock.simulation_time + @debug "New time" time(container) return SimulationResult(elapsed, messaging_sim_result, task_sim_result, time_step_s) end @@ -321,16 +329,16 @@ of the simulation of `max_advance_time_s`. This function will step the container until the clock has advanced to the initial_time + `max_advance_time_s` or if the time of the container does not advance anymore (which would mean no events are scheduled). """ -function discrete_event_simulation(container::SimulationContainer, max_advance_time_s::Real) - initial_time = container.clock.simulation_time +function discrete_step_until(container::SimulationContainer, max_advance_time_s::Real) + initial_time = time(container) prev_time = nothing results = [] - while isnothing(prev_time) || (prev_time < container.clock.simulation_time + while isnothing(prev_time) || ((prev_time < time(container) || length(results) == 1) && - initial_time + Second(max_advance_time_s) <= container.clock.simulation_time) + initial_time + Second(max_advance_time_s) > time(container)) - prev_time = container.clock.simulation_time + prev_time = time(container) push!(results, step_simulation(container)) end return results @@ -385,7 +393,7 @@ 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)) + push!(container.message_queue, MessageData(msg, meta, time(container))) return NonWaitable() end @@ -424,4 +432,24 @@ function Base.getindex(container::SimulationContainer, index::String) end function Base.getindex(container::SimulationContainer, index::Int) return agents(container)[index] +end + +function env(container::SimulationContainer) + return env(container.world) +end + +function space(container::SimulationContainer) + return space(container.world) +end + +function world(container::SimulationContainer) + return container.world +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/util/scheduling.jl b/src/util/scheduling.jl index b910bb42..72d9357e 100644 --- a/src/util/scheduling.jl +++ b/src/util/scheduling.jl @@ -195,7 +195,7 @@ function execute_task(f::Function, scheduler::AbstractScheduler, data::InstantTa 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 @@ -383,7 +383,7 @@ function sleep(scheduler::SimulationScheduler, time_s::Real) 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) + @debug "Sleep task with" current_task() event ctime time_s wait(event) end diff --git a/src/world/core.jl b/src/world/core.jl index 77ce3709..4271181c 100644 --- a/src/world/core.jl +++ b/src/world/core.jl @@ -1,4 +1,6 @@ -export World, Space, Position, Position2D, Area2D, location, move, initialize, initialized +export World, Space, Position, Position2D, Area2D, location, + move, initialize, initialized, Environment, schedule, WorldObserver, + emit_global_event, env abstract type Position end abstract type Space{P<:Position} end @@ -9,7 +11,7 @@ function dispatch_global_event(observer::WorldObserver, event::Any) # default no reaction end -struct NoEnv end +struct NoEnv <: Environment end struct Position2D <: Position x::Real @@ -22,29 +24,52 @@ end to_position::Dict{String,Position2D} = Dict() end -@kwdef struct World{S<:Space} +""" +Struct World. The world is meant to provide a description of everything which exists outside of the agents. + +The world is a separate entity, which describes some type of world, 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 World{S<:Space} + scheduler::SimulationScheduler space::S = Area2D(width=10, height=10) environment::Environment = NoEnv() - scheduler::AbstractScheduler = SimulationScheduler() - observers::Vector{WorldObserver} = Vector() + observers::Vector{WorldObserver} = Vector{WorldObserver}() + data_selectors::Vector{Function} = Vector{Function}() initialized::Bool = false end schedule(f::Function, world::World, data::TaskData) = schedule(f, world.scheduler, data) +""" + on_step(world::World, clock::Clock, step_size_s::Real) + +Called on stepping the container. +""" function on_step(world::World, clock::Clock, step_size_s::Real) # default do nothing end +""" + on_step(environment::Environment, world::World, clock::Clock, step_size_s::Real) + +Called on stepping the container. +""" function on_step(environment::Environment, world::World, clock::Clock, step_size_s::Real) # default do nothing end function step(world::World, clock::Clock, step_size_s::Real) on_step(world, clock, step_size_s) - on_step(environment, world, clock, step_size_s) + on_step(env(world), world, clock, step_size_s) 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 @@ -53,6 +78,11 @@ function location(space::Area2D, agent::Agent)::Position2D return space.to_position[aid(agent)] 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 @@ -75,15 +105,31 @@ function initialize(world::World{S}, agents::Vector{A}) where {S<:Space} where { initialize(world.space, agents) end +""" + initialized(world::World) + +Return whether the world is intialized. +""" function initialized(world::World) return world.initialized end -function add_observer!(world::World, observer::Any) +""" + add_observer!(world::World, observer::WorldObserver) + +Add an observer to the world, which is able to handle +global event emitted by the world. +""" +function add_observer!(world::World, observer::WorldObserver) push!(world.observers, observer) end -function environment(world::World) +""" + env(world::World) + +Return the environment of the world. +""" +function env(world::World) return world.environment end @@ -100,3 +146,13 @@ function emit_global_event(world::World, event::Any) dispatch_global_event(observer, event) end end + +""" + select(world::World, selector::Function) + +Select an output attribute, which will be recorded while +the simulation is running (every step!). +""" +function select!(world::World, selector::Function) + push!(world.data_selectors, selector) +end \ No newline at end of file From f571a8c2af4e3e107f50616948a1d6ad4996b0e6 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Fri, 6 Dec 2024 17:57:44 +0100 Subject: [PATCH 17/54] Renaming world to environment, adding some theme overrides to the docs. --- README.md | 4 +- docs/Project.toml | 4 +- docs/make.jl | 4 +- .../logo-dark.svg} | 0 .../logo.svg} | 0 docs/src/assets/mango_theme_overrides.css | 63 ++++++ docs/src/simulation.md | 4 +- src/Mango.jl | 2 +- src/express/api.jl | 4 +- src/util/scheduling.jl | 2 +- src/world/core.jl | 78 +++---- src/{container => world}/simulation.jl | 202 +++++++++--------- test/agent_modeling_tests.jl | 18 +- test/express_api_tests.jl | 2 +- test/simulation_container_tests.jl | 32 +-- 15 files changed, 239 insertions(+), 180 deletions(-) rename docs/src/{Logo_mango_ohne_sub_white.svg => assets/logo-dark.svg} (100%) rename docs/src/{Logo_mango_ohne_sub.svg => assets/logo.svg} (100%) create mode 100644 docs/src/assets/mango_theme_overrides.css rename src/{container => world}/simulation.jl (59%) diff --git a/README.md b/README.md index 884e5f76..020f7fb1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

-![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)

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 bdd8b1ab..0dec904f 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/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..8fdb736b --- /dev/null +++ b/docs/src/assets/mango_theme_overrides.css @@ -0,0 +1,63 @@ + +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; +} \ No newline at end of file diff --git a/docs/src/simulation.md b/docs/src/simulation.md index a62c2af9..83932c38 100644 --- a/docs/src/simulation.md +++ b/docs/src/simulation.md @@ -4,7 +4,7 @@ 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,7 +18,7 @@ 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()) diff --git a/src/Mango.jl b/src/Mango.jl index 9feab4ec..39572298 100644 --- a/src/Mango.jl +++ b/src/Mango.jl @@ -20,9 +20,9 @@ include("container/mqtt.jl") include("simulation/communication.jl") include("simulation/tasks.jl") -include("container/simulation.jl") include("container/core.jl") +include("world/simulation.jl") include("world/topology.jl") include("express/api.jl") diff --git a/src/express/api.jl b/src/express/api.jl index d6065035..b441835c 100644 --- a/src/express/api.jl +++ b/src/express/api.jl @@ -312,12 +312,12 @@ 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_container = create_world(start_time, communication_sim=communication_sim) for agent in agents register(sim_container, agent) end diff --git a/src/util/scheduling.jl b/src/util/scheduling.jl index 72d9357e..c08f6bb2 100644 --- a/src/util/scheduling.jl +++ b/src/util/scheduling.jl @@ -68,7 +68,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 """ diff --git a/src/world/core.jl b/src/world/core.jl index 4271181c..609709ca 100644 --- a/src/world/core.jl +++ b/src/world/core.jl @@ -1,17 +1,17 @@ -export World, Space, Position, Position2D, Area2D, location, - move, initialize, initialized, Environment, schedule, WorldObserver, +export Environment, Space, Position, Position2D, Area2D, location, + move, initialize, initialized, Behavior, schedule, WorldObserver, emit_global_event, env abstract type Position end abstract type Space{P<:Position} end abstract type WorldObserver end -abstract type Environment end +abstract type Behavior end function dispatch_global_event(observer::WorldObserver, event::Any) # default no reaction end -struct NoEnv <: Environment end +struct NoBehavior <: Behavior end struct Position2D <: Position x::Real @@ -25,44 +25,44 @@ end end """ -Struct World. The world is meant to provide a description of everything which exists outside of the agents. +Struct Environment. The environment is meant to provide a description of everything which exists outside of the agents. -The world is a separate entity, which describes some type of world, this can be anything which exists in +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 World{S<:Space} +@kwdef mutable struct Environment{S<:Space} scheduler::SimulationScheduler space::S = Area2D(width=10, height=10) - environment::Environment = NoEnv() + behavior::Behavior = NoBehavior() observers::Vector{WorldObserver} = Vector{WorldObserver}() data_selectors::Vector{Function} = Vector{Function}() initialized::Bool = false end -schedule(f::Function, world::World, data::TaskData) = schedule(f, world.scheduler, data) +schedule(f::Function, environment::Environment, data::TaskData) = schedule(f, environment.scheduler, data) """ - on_step(world::World, clock::Clock, step_size_s::Real) + on_step(environment::Environment, clock::Clock, step_size_s::Real) Called on stepping the container. """ -function on_step(world::World, clock::Clock, step_size_s::Real) +function on_step(environment::Environment, clock::Clock, step_size_s::Real) # default do nothing end """ - on_step(environment::Environment, world::World, clock::Clock, step_size_s::Real) + on_step(behavior::Behavior, environment::Environment, clock::Clock, step_size_s::Real) Called on stepping the container. """ -function on_step(environment::Environment, world::World, clock::Clock, step_size_s::Real) +function on_step(behavior::Behavior, environment::Environment, clock::Clock, step_size_s::Real) # default do nothing end -function step(world::World, clock::Clock, step_size_s::Real) - on_step(world, clock, step_size_s) - on_step(env(world), world, clock, step_size_s) +function step(env::Environment, clock::Clock, step_size_s::Real) + on_step(env, clock, step_size_s) + on_step(behavior(env), env, clock, step_size_s) end """ @@ -101,58 +101,58 @@ function initialize(space::Area2D, agents::Vector{A}) where {A<:Agent} end end -function initialize(world::World{S}, agents::Vector{A}) where {S<:Space} where {A<:Agent} - initialize(world.space, agents) +function initialize(environment::Environment{S}, agents::Vector{A}) where {S<:Space} where {A<:Agent} + initialize(environment.space, agents) end """ - initialized(world::World) + initialized(environment::Environment) -Return whether the world is intialized. +Return whether the environment is intialized. """ -function initialized(world::World) - return world.initialized +function initialized(environment::Environment) + return environment.initialized end """ - add_observer!(world::World, observer::WorldObserver) + add_observer!(environment::Environment, observer::WorldObserver) -Add an observer to the world, which is able to handle -global event emitted by the world. +Add an observer to the environment, which is able to handle +global event emitted by the environment. """ -function add_observer!(world::World, observer::WorldObserver) - push!(world.observers, observer) +function add_observer!(environment::Environment, observer::WorldObserver) + push!(environment.observers, observer) end """ - env(world::World) + behavior(env::Environment) -Return the environment of the world. +Return the behavior of the environment. """ -function env(world::World) - return world.environment +function behavior(env::Environment) + return env.behavior end """ - emit_global_event(world::World, event::Any) + emit_global_event(environment::Environment, event::Any) -Emit a global world event. This types of events can be handled by any agent -living in the world (resp. living in the container, the world exists in). +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(world::World, event::Any) - for observer in world.observers +function emit_global_event(environment::Environment, event::Any) + for observer in environment.observers dispatch_global_event(observer, event) end end """ - select(world::World, selector::Function) + select(environment::Environment, selector::Function) Select an output attribute, which will be recorded while the simulation is running (every step!). """ -function select!(world::World, selector::Function) - push!(world.data_selectors, selector) +function select!(environment::Environment, selector::Function) + push!(environment.data_selectors, selector) end \ No newline at end of file diff --git a/src/container/simulation.jl b/src/world/simulation.jl similarity index 59% rename from src/container/simulation.jl rename to src/world/simulation.jl index c339fbde..b89da7d7 100644 --- a/src/container/simulation.jl +++ b/src/world/simulation.jl @@ -1,6 +1,6 @@ -export SimulationContainer, register, send_message, shutdown, protocol_addr, - create_simulation_container, step_simulation, SimulationResult, CommunicationSimulationResult, - TaskSimulationResult, on_step, discrete_step_until, env, space, world, time, clock +export World, register, send_message, shutdown, protocol_addr, + create_world, step_simulation, SimulationResult, CommunicationSimulationResult, + TaskSimulationResult, on_step, discrete_step_until, env, space, time, clock using Base.Threads using Dates @@ -21,37 +21,37 @@ 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_world(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`. +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_simulation_container(start_time::DateTime; +function create_world(start_time::DateTime; communication_sim::Union{Nothing,CommunicationSimulation}=nothing, task_sim::Union{Nothing,TaskSimulation}=nothing, space::Union{Nothing,Space}=nothing, - env::Union{Nothing,Environment}=nothing) + behavior::Union{Nothing,Behavior}=nothing) - container = SimulationContainer() - container.clock.simulation_time = start_time + world = World() + world.clock.simulation_time = start_time if !isnothing(communication_sim) - container.communication_sim = communication_sim + world.communication_sim = communication_sim end if !isnothing(task_sim) - container.task_sim = task_sim + world.task_sim = task_sim end if !isnothing(space) - container.world.space = space + world.env.space = space end - if !isnothing(env) - container.world.environment = env + if !isnothing(behavior) + world.env.behavior = behavior end - add_observer!(container.world, container.world_observer) - add_simulation_scheduler!(container.task_sim, container.world.scheduler) - return container + add_observer!(world.env, world.world_observer) + add_simulation_scheduler!(world.task_sim, world.env.scheduler) + return world end """ @@ -74,11 +74,11 @@ function dispatch_global_event(observer::DispatchToAgentWorldObserver, event::An end """ -The SimulationContainer used as a base struct to enable simulations in Mango.jl. Always create using [`create_simulation_container`](@ref). +The World used as a base struct to enable simulations in Mango.jl. Always create using [`create_world`](@ref). """ -@kwdef mutable struct SimulationContainer <: ContainerInterface +@kwdef mutable struct World <: ContainerInterface clock::Clock = Clock(DateTime(0)) - world::World = World(scheduler=SimulationScheduler(clock=clock)) + env::Environment = Environment(scheduler=SimulationScheduler(clock=clock)) task_sim::TaskSimulation = SimpleTaskSimulation(clock=clock) agents::OrderedDict{String,Agent} = OrderedDict{String,Agent}() agent_counter::Integer = 0 @@ -88,40 +88,40 @@ The SimulationContainer used as a base struct to enable simulations in Mango.jl. world_observer::WorldObserver = DispatchToAgentWorldObserver(agents) end -function agents(container::SimulationContainer)::Vector{Agent} - return [t[2] for t in collect(container.agents)] +function agents(world::World)::Vector{Agent} + return [t[2] for t in collect(world.agents)] end """ - on_step(agent::Agent, world::World, clock::Clock, step_size_s::Real) + on_step(agent::Agent, env::Environment, clock::Clock, step_size_s::Real) -Hook-in, called on every step of the simulation container for every `agent`. +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, world::World, clock::Clock, step_size_s::Real) +function on_step(agent::Agent, env::Environment, clock::Clock, step_size_s::Real) # default nothing end -function on_step(role::Role, world::World, clock::Clock, step_size_s::Real) +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, world::World, clock::Clock, step_size_s::Real) - on_step(agent, world, clock, step_size_s) +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, world, clock, step_size_s) + on_step(role, env, clock, step_size_s) end end """ Contains the result of the communication simulation and whether the state of -the container has changed +the world has changed """ struct MessagingIterationResult communication_result::CommunicationSimulationResult @@ -194,25 +194,25 @@ end """ Internal """ -function cs_step_iteration(container::SimulationContainer, +function cs_step_iteration(world::World, step_size_s::Real, pre_communication_result::Union{Nothing,CommunicationSimulationResult})::MessagingIterationResult - message_packages = to_cs_input!(container.message_queue) + message_packages = to_cs_input!(world.message_queue) communication_result = pre_communication_result if isnothing(communication_result) - communication_result = calculate_communication(container.communication_sim, - clock(container), + 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(container), step_size_s) && pr.reached + if add_seconds(mp.sent_date, pr.delay_s) <= add_seconds(time(world), step_size_s) && pr.reached state_changed = true - @spawnlog process_message(container, mp.content[1], mp.content[2]) + @spawnlog process_message(world, mp.content[1], mp.content[2]) else # process it later - push!(container.message_queue, MessageData(mp.content[1], mp.content[2], mp.sent_date)) + push!(world.message_queue, MessageData(mp.content[1], mp.content[2], mp.sent_date)) end end end @@ -222,20 +222,20 @@ end """ Internal """ -function determine_time_step(container::SimulationContainer) - message_packages = to_cs_input(container.message_queue) - communication_result = calculate_communication(container.communication_sim, clock(container), message_packages) +function determine_time_step(world::World) + message_packages = to_cs_input(world.message_queue) + 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(container)).value / 1000 + 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(container.task_sim) + next_event_s = determine_next_event_time(world.task_sim) @debug "Next event in $next_event_s" @@ -253,22 +253,22 @@ function determine_time_step(container::SimulationContainer) end """ - step_simulation(container::SimulationContainer, step_size_s::Real=DISCRETE_EVENT)::Union{SimulationResult,Nothing} + step_simulation(world::World, 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} +function step_simulation(world::World, 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)]) + if !initialized(world.env) + initialize(world.env, [v for v in values(world.agents)]) end state_changed = true - @debug "Time at the start of the step" time(container) + @debug "Time at the start of the step" time(world) task_sim_result = TaskSimulationResult() messaging_sim_result = MessagingSimulationResult() @@ -280,7 +280,7 @@ function step_simulation(container::SimulationContainer, step_size_s::Real=DISCR # 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) + time_step_s, comm_result = determine_time_step(world) @debug "Determined the size to be $time_step_s" if isnothing(time_step_s) return nothing @@ -294,8 +294,8 @@ function step_simulation(container::SimulationContainer, step_size_s::Real=DISCR 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) + Threads.@spawn comm_iter_result = cs_step_iteration(world, time_step_s, first_step ? comm_result : nothing) + Threads.@spawn task_iter_result = step_iteration(world.task_sim, time_step_s, first_step) end first_step = false push!(task_sim_result.results, task_iter_result) @@ -304,87 +304,87 @@ function step_simulation(container::SimulationContainer, step_size_s::Real=DISCR @debug "Finish simulation iteration" state_changed end - step(container.world, clock(container), time_step_s) + step(world.env, clock(world), time_step_s) # agents act on the stepping hook - for agent in values(container.agents) - step_agent(agent, container.world, clock(container), time_step_s) + for agent in values(world.agents) + step_agent(agent, world.env, clock(world), time_step_s) end end @debug "The simulation step needed $elapsed seconds" - container.clock.simulation_time = add_seconds(time(container), time_step_s) + world.clock.simulation_time = add_seconds(time(world), time_step_s) - @debug "New time" time(container) + @debug "New time" time(world) return SimulationResult(elapsed, messaging_sim_result, task_sim_result, time_step_s) end """ - discrete_event_simulation(container::SimulationContainer, max_advance_time_s::Real) + discrete_event_simulation(world::World, max_advance_time_s::Real) -Execute a discrete event simulation using the `container` with the maximal allowed advanced time +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 container until the clock has advanced to the initial_time + `max_advance_time_s` -or if the time of the container does not advance anymore (which would mean no events are scheduled). +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(container::SimulationContainer, max_advance_time_s::Real) - initial_time = time(container) +function discrete_step_until(world::World, max_advance_time_s::Real) + initial_time = time(world) prev_time = nothing results = [] - while isnothing(prev_time) || ((prev_time < time(container) || length(results) == 1) + while isnothing(prev_time) || ((prev_time < time(world) || length(results) == 1) && - initial_time + Second(max_advance_time_s) > time(container)) + initial_time + Second(max_advance_time_s) > time(world)) - prev_time = time(container) - push!(results, step_simulation(container)) + prev_time = time(world) + push!(results, step_simulation(world)) end return results end -function protocol_addr(container::SimulationContainer) +function protocol_addr(world::World) return nothing end -function shutdown(container::SimulationContainer) - container.shutdown = true +function shutdown(world::World) + world.shutdown = true - for agent in values(container.agents) + for agent in values(world.agents) shutdown(agent) end end function register( - container::SimulationContainer, + world::World, 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::String = "$AGENT_PREFIX$(world.agent_counter)" + if !isnothing(suggested_aid) && !haskey(world.agents, suggested_aid) actual_aid = suggested_aid end - container.agents[actual_aid] = agent + world.agents[actual_aid] = agent agent.aid = actual_aid - agent.context = AgentContext(container) - container.agent_counter += 1 + agent.context = AgentContext(world) + world.agent_counter += 1 - if !isnothing(container.task_sim) - agent.scheduler = create_agent_scheduler(container.task_sim) + if !isnothing(world.task_sim) + agent.scheduler = create_agent_scheduler(world.task_sim) end return agent end -function process_message(container::SimulationContainer, msg::Any, meta::AbstractDict) +function process_message(world::World, 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 + if !haskey(world.agents, meta[RECEIVER_ID]) + @warn "Container $(keys(world.agents)) has no agent with id: $receiver_id" msg meta else - agent = container.agents[receiver_id] + agent = world.agents[receiver_id] return dispatch_message(agent, msg, meta) end end @@ -392,13 +392,13 @@ 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, time(container))) +function forward_message(world::World, msg::Any, meta::AbstractDict) + push!(world.message_queue, MessageData(msg, meta, time(world))) return NonWaitable() end function send_message( - container::SimulationContainer, + world::World, content::Any, agent_adress::AgentAddress, sender_id::Union{Nothing,String}=nothing; @@ -419,37 +419,33 @@ function send_message( @debug "Send a message to ($receiver_id), from $sender_id" typeof(content) - return forward_message(container, content, meta) + return forward_message(world, content, meta) end """ - Base.getindex(container::SimulationContainer, index::String) + Base.getindex(world::World, index::String) -Return the agent indexed by `index` (aid). +Return the agent indexed by `index` (aid). """ -function Base.getindex(container::SimulationContainer, index::String) - return container.agents[index] +function Base.getindex(world::World, index::String) + return world.agents[index] end -function Base.getindex(container::SimulationContainer, index::Int) - return agents(container)[index] +function Base.getindex(world::World, index::Int) + return agents(world)[index] end -function env(container::SimulationContainer) - return env(container.world) +function env(world::World) + return env(world.env) end -function space(container::SimulationContainer) - return space(container.world) +function space(world::World) + return space(world.env) end -function world(container::SimulationContainer) - return container.world +function clock(world::World) + return world.clock end -function clock(container::SimulationContainer) - return container.clock -end - -function time(container::SimulationContainer) - return clock(container).simulation_time +function time(world::World) + return clock(world).simulation_time 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/express_api_tests.jl b/test/express_api_tests.jl index 9ca899f2..e8a6379d 100644 --- a/test/express_api_tests.jl +++ b/test/express_api_tests.jl @@ -175,7 +175,7 @@ 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)) diff --git a/test/simulation_container_tests.jl b/test/simulation_container_tests.jl index 159f048b..c8fcc953 100644 --- a/test/simulation_container_tests.jl +++ b/test/simulation_container_tests.jl @@ -17,9 +17,9 @@ function handle_message(agent::SimAgent, message::Any, meta::AbstractDict) end end -@testset "SimulationContainerKwargs" begin +@testset "WorldKwargs" 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 = SimAgent(0) agent2 = SimAgent(0) register(container, agent1) @@ -34,16 +34,16 @@ end @test agent1.counter == 11 end -@testset "SimulationContainerNoProtocolSpecificAddr" begin +@testset "WorldNoProtocolSpecificAddr" 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)) @test isnothing(protocol_addr(container)) end -@testset "SimulationContainerNoValidTargetCustomAid" begin +@testset "WorldNoValidTargetCustomAid" 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 = SimAgent(0) agent2 = SimAgent(0) register(container, agent1) @@ -62,7 +62,7 @@ end @testset "SimpleInternalSimulationWithoutDelayContainerTest" 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 = SimAgent(0) agent2 = SimAgent(0) register(container, agent1) @@ -82,7 +82,7 @@ end @testset "SimpleInternalSimulationDelayGreaterStepSize" begin - container = create_simulation_container(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) + container = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) agent1 = SimAgent(0) agent2 = SimAgent(0) register(container, agent1) @@ -104,7 +104,7 @@ end @testset "SimpleInternalSimulationDelayMixedGreaterStepSize" begin - container = create_simulation_container(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) + container = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) agent1 = SimAgent(0) agent2 = SimAgent(0) register(container, agent1) @@ -135,7 +135,7 @@ end @testset "SimpleInternalSimulationLinkSpecificDelay" begin com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_simulation_container(DateTime(Millisecond(0)), communication_sim=com_sim) + container = create_world(DateTime(Millisecond(0)), communication_sim=com_sim) agent1 = SimAgent(0) agent2 = SimAgent(0) register(container, agent1) @@ -170,7 +170,7 @@ end @testset "SimulationWithSpecificDelaysAndScheduledTasks" begin com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_simulation_container(DateTime(0), communication_sim=com_sim) + container = create_world(DateTime(0), communication_sim=com_sim) agent1 = SimSchedulingAgent(0, 0) agent2 = SimSchedulingAgent(0, 0) register(container, agent1) @@ -215,7 +215,7 @@ end @testset "SimulationWithSpecificDelaysAndScheduledTasksOnHandle" begin com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_simulation_container(DateTime(0), communication_sim=com_sim) + container = create_world(DateTime(0), communication_sim=com_sim) agent1 = ComplexSimSchedulingAgent(0, 0) agent2 = ComplexSimSchedulingAgent(0, 0) register(container, agent1) @@ -262,7 +262,7 @@ end @testset "SimulationWithSpecificDelaysWithReplyOnHandle" begin com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_simulation_container(DateTime(0), communication_sim=com_sim) + container = create_world(DateTime(0), communication_sim=com_sim) agent1 = MoreComplexSimSchedulingAgent(0, 0) agent2 = MoreComplexSimSchedulingAgent(0, 0) register(container, agent1) @@ -294,7 +294,7 @@ end @testset "SimulationWithSpecificDelaysWithReplyOnHandleDiscreteEvent" begin com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_simulation_container(DateTime(0), communication_sim=com_sim) + container = create_world(DateTime(0), communication_sim=com_sim) agent1 = MoreComplexSimSchedulingAgent(0, 0) agent2 = MoreComplexSimSchedulingAgent(0, 0) register(container, agent1) @@ -379,8 +379,8 @@ end @test_throws "Please implement determine_next_event_time(...)" Mango.determine_next_event_time(TestTaskSim()) end -@testset "SimulationContainerAgentsAreOrdered" begin - container = create_simulation_container(DateTime(0)) +@testset "WorldAgentsAreOrdered" begin + container = create_world(DateTime(0)) a1 = register(container, SimAgent(0)) a2 = register(container, SimAgent(1)) a3 = register(container, SimAgent(2)) From aebde542f2c3774df42e5b03bdfb79702ab9631f Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Sun, 8 Dec 2024 22:40:40 +0100 Subject: [PATCH 18/54] Testing and fixing world observer. --- src/agent/core.jl | 2 +- src/world/core.jl | 9 --------- src/world/simulation.jl | 14 +++++++------- test/environment_api_tests.jl | 29 +++++++++++++++++++++++++++++ test/role_tests.jl | 20 ++++++++++++++++++++ test/runtests.jl | 1 + 6 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 test/environment_api_tests.jl diff --git a/src/agent/core.jl b/src/agent/core.jl index bfef9180..0f7cb5c2 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -525,7 +525,7 @@ function Base.getindex(agent::T, index::Int) where {T<:Agent} return roles(agent)[index] end -function Base.getindex(agent::T, index::DataType) where {T<:Agent} +function Base.getindex(agent::T, index::Type) where {T<:Agent} for role in roles(agent) if typeof(role) == index return role diff --git a/src/world/core.jl b/src/world/core.jl index 609709ca..4c185199 100644 --- a/src/world/core.jl +++ b/src/world/core.jl @@ -42,14 +42,6 @@ end schedule(f::Function, environment::Environment, data::TaskData) = schedule(f, environment.scheduler, data) -""" - on_step(environment::Environment, clock::Clock, step_size_s::Real) - -Called on stepping the container. -""" -function on_step(environment::Environment, clock::Clock, step_size_s::Real) - # default do nothing -end """ on_step(behavior::Behavior, environment::Environment, clock::Clock, step_size_s::Real) @@ -61,7 +53,6 @@ function on_step(behavior::Behavior, environment::Environment, clock::Clock, ste end function step(env::Environment, clock::Clock, step_size_s::Real) - on_step(env, clock, step_size_s) on_step(behavior(env), env, clock, step_size_s) end diff --git a/src/world/simulation.jl b/src/world/simulation.jl index b89da7d7..708ee4a1 100644 --- a/src/world/simulation.jl +++ b/src/world/simulation.jl @@ -275,6 +275,13 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ first_step = true time_step_s = step_size_s + step(world.env, clock(world), time_step_s) + + # agents act on the stepping hook + for agent in values(world.agents) + step_agent(agent, world.env, clock(world), time_step_s) + end + # 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 @@ -303,13 +310,6 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ state_changed = comm_iter_result.state_changed || task_iter_result.state_changed @debug "Finish simulation iteration" state_changed end - - step(world.env, clock(world), time_step_s) - - # agents act on the stepping hook - for agent in values(world.agents) - step_agent(agent, world.env, clock(world), time_step_s) - end end @debug "The simulation step needed $elapsed seconds" diff --git a/test/environment_api_tests.jl b/test/environment_api_tests.jl new file mode 100644 index 00000000..5a0df115 --- /dev/null +++ b/test/environment_api_tests.jl @@ -0,0 +1,29 @@ +using Mango +using Test +using Dates + +@agent struct WorldEventAgent + counter::Real +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::Environment, clock::Clock, step_size_s::Real) + emit_global_event(environment, "Hello Agent, I am the environment") +end + +@testset "TestAgentWorldEvent" begin + container = create_world(DateTime(Millisecond(23)), + communication_sim=SimpleCommunicationSimulation(default_delay_s=0), + behavior=TestBehavior()) + agent1 = WorldEventAgent(0) + register(container, agent1) + + stepping_result = step_simulation(container) + + @test agent1.counter == 7 +end \ No newline at end of file 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..6e9d90fb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -14,5 +14,6 @@ using Documenter include("agent_modeling_tests.jl") include("express_api_tests.jl") include("topology_tests.jl") + include("environment_api_tests.jl") doctest(Mango) end \ No newline at end of file From 9542f0f6a540f3ebbfdadf9fe294e5bfdb15134d Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Sun, 8 Dec 2024 23:08:11 +0100 Subject: [PATCH 19/54] More tests. --- test/environment_api_tests.jl | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/environment_api_tests.jl b/test/environment_api_tests.jl index 5a0df115..a09e76d1 100644 --- a/test/environment_api_tests.jl +++ b/test/environment_api_tests.jl @@ -5,15 +5,23 @@ 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 + agent.counter += 7 end struct TestBehavior <: Behavior end function Mango.on_step(behavior::TestBehavior, environment::Environment, 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 @@ -22,8 +30,12 @@ end behavior=TestBehavior()) agent1 = WorldEventAgent(0) register(container, agent1) + agent2 = add_agent_composed_of(container, WorldEventRole(1)) stepping_result = step_simulation(container) - - @test agent1.counter == 7 + @test agent1.counter == 14 + @test agent2[WorldEventRole].counter == 15 + stepping_result = step_simulation(container) + @test agent1.counter == 28 + @test agent2[WorldEventRole].counter == 29 end \ No newline at end of file From c52dfb6c90f274dc72208fdb22b5dd4a2e0112a4 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 10 Dec 2024 16:16:40 +0100 Subject: [PATCH 20/54] Extracted SimulationContainer from World. --- docs/src/assets/mango_theme_overrides.css | 74 ++++--- src/Mango.jl | 8 +- src/{world => environment}/core.jl | 1 + src/express/api.jl | 10 +- src/simulation/container.jl | 118 ++++++++++++ .../simulation.jl => simulation/world.jl} | 132 +++---------- src/{world => util}/topology.jl | 0 src/util/visualization.jl | 3 + test/environment_api_tests.jl | 10 +- test/express_api_tests.jl | 2 +- test/runtests.jl | 2 +- ...tion_container_tests.jl => world_tests.jl} | 181 +++++++++--------- 12 files changed, 309 insertions(+), 232 deletions(-) rename src/{world => environment}/core.jl (99%) create mode 100644 src/simulation/container.jl rename src/{world/simulation.jl => simulation/world.jl} (81%) rename src/{world => util}/topology.jl (100%) create mode 100644 src/util/visualization.jl rename test/{simulation_container_tests.jl => world_tests.jl} (57%) diff --git a/docs/src/assets/mango_theme_overrides.css b/docs/src/assets/mango_theme_overrides.css index 8fdb736b..20ebbcc7 100644 --- a/docs/src/assets/mango_theme_overrides.css +++ b/docs/src/assets/mango_theme_overrides.css @@ -1,17 +1,19 @@ - body { - font-family: -apple-system, BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI emojiEmoji; + 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; + 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 { +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; } @@ -22,21 +24,22 @@ html.theme--documenter-dark .select select, html.theme--documenter-dark .textare @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; + 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; } } @@ -46,18 +49,43 @@ html.theme--documenter-dark #documenter .docs-sidebar ul.docs-menu .tocitem { @media screen and (min-width: 1056px) { #documenter .docs-main { - max-width: 52rem; - padding-right: 1rem; - margin-left: 50px; + 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/src/Mango.jl b/src/Mango.jl index 39572298..9aa6a19f 100644 --- a/src/Mango.jl +++ b/src/Mango.jl @@ -12,7 +12,6 @@ include("container/api.jl") include("agent/role.jl") include("agent/core.jl") -include("world/core.jl") include("container/protocol.jl") include("container/tcp.jl") @@ -22,8 +21,11 @@ include("simulation/communication.jl") include("simulation/tasks.jl") include("container/core.jl") -include("world/simulation.jl") -include("world/topology.jl") +include("simulation/container.jl") +include("environment/core.jl") +include("simulation/world.jl") +include("util/topology.jl") +include("util/visualization.jl") include("express/api.jl") diff --git a/src/world/core.jl b/src/environment/core.jl similarity index 99% rename from src/world/core.jl rename to src/environment/core.jl index 4c185199..f1139945 100644 --- a/src/world/core.jl +++ b/src/environment/core.jl @@ -13,6 +13,7 @@ end struct NoBehavior <: Behavior end + struct Position2D <: Position x::Real y::Real diff --git a/src/express/api.jl b/src/express/api.jl index b441835c..0a15087d 100644 --- a/src/express/api.jl +++ b/src/express/api.jl @@ -317,15 +317,15 @@ runnable the simulation container is stepped `n_steps` time with a `step_size_s` 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_world(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 diff --git a/src/simulation/container.jl b/src/simulation/container.jl new file mode 100644 index 00000000..f2f0bc72 --- /dev/null +++ b/src/simulation/container.jl @@ -0,0 +1,118 @@ + + +""" +Represents a message data package including the arriving time of the package. +""" +struct MessageData + content::Any + meta::AbstractDict + arriving_time::DateTime +end + +@kwdef mutable struct SimulationContainer <: ContainerInterface + clock::Clock + 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 + agent.aid = actual_aid + agent.context = AgentContext(container) + container.agent_counter += 1 + + return agent +end + +function forward_message(container::SimulationContainer, msg::Any, meta::AbstractDict) + push!(container.message_queue, MessageData(msg, meta, time(container))) + 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 "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 + +""" + 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/world/simulation.jl b/src/simulation/world.jl similarity index 81% rename from src/world/simulation.jl rename to src/simulation/world.jl index 708ee4a1..688bc887 100644 --- a/src/world/simulation.jl +++ b/src/simulation/world.jl @@ -54,15 +54,6 @@ function create_world(start_time::DateTime; return world end -""" -Represents a message data package including the arriving time of the package. -""" -struct MessageData - content::Any - meta::AbstractDict - arriving_time::DateTime -end - struct DispatchToAgentWorldObserver <: WorldObserver agents_ref::OrderedDict{String,Agent} end @@ -78,18 +69,15 @@ The World used as a base struct to enable simulations in Mango.jl. Always create """ @kwdef mutable struct World <: ContainerInterface clock::Clock = Clock(DateTime(0)) + container::SimulationContainer = SimulationContainer(clock=clock) env::Environment = Environment(scheduler=SimulationScheduler(clock=clock)) task_sim::TaskSimulation = SimpleTaskSimulation(clock=clock) - agents::OrderedDict{String,Agent} = OrderedDict{String,Agent}() - agent_counter::Integer = 0 - shutdown::Bool = false communication_sim::CommunicationSimulation = SimpleCommunicationSimulation() - message_queue::ConcurrentQueue{MessageData} = ConcurrentQueue{MessageData}() - world_observer::WorldObserver = DispatchToAgentWorldObserver(agents) + world_observer::WorldObserver = DispatchToAgentWorldObserver(container.agents) end function agents(world::World)::Vector{Agent} - return [t[2] for t in collect(world.agents)] + return agents(world.container) end """ @@ -197,7 +185,7 @@ Internal function cs_step_iteration(world::World, step_size_s::Real, pre_communication_result::Union{Nothing,CommunicationSimulationResult})::MessagingIterationResult - message_packages = to_cs_input!(world.message_queue) + message_packages = to_cs_input!(messages(world.container)) communication_result = pre_communication_result if isnothing(communication_result) communication_result = calculate_communication(world.communication_sim, @@ -209,10 +197,10 @@ function cs_step_iteration(world::World, 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 - @spawnlog process_message(world, mp.content[1], mp.content[2]) + @spawnlog process_message(world.container, mp.content[1], mp.content[2]) else # process it later - push!(world.message_queue, MessageData(mp.content[1], mp.content[2], mp.sent_date)) + push!(messages(world.container), MessageData(mp.content[1], mp.content[2], mp.sent_date)) end end end @@ -223,7 +211,7 @@ end Internal """ function determine_time_step(world::World) - message_packages = to_cs_input(world.message_queue) + 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 @@ -263,7 +251,7 @@ DISCRETE_EVENT has to be set for the `step_size_s`. function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{SimulationResult,Nothing} # Init world if uninitialized if !initialized(world.env) - initialize(world.env, [v for v in values(world.agents)]) + initialize(world.env, [v for v in values(agents(world))]) end state_changed = true @@ -278,7 +266,7 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ step(world.env, clock(world), time_step_s) # agents act on the stepping hook - for agent in values(world.agents) + for agent in values(agents(world)) step_agent(agent, world.env, clock(world), time_step_s) end @@ -344,16 +332,23 @@ function discrete_step_until(world::World, max_advance_time_s::Real) return results end -function protocol_addr(world::World) - return nothing +struct NonWaitable end +function Base.wait(waitable::NonWaitable) end + +function env(world::World) + return env(world.env) end -function shutdown(world::World) - world.shutdown = true +function space(world::World) + return space(world.env) +end - for agent in values(world.agents) - shutdown(agent) - end +function clock(world::World) + return world.clock +end + +function time(world::World) + return clock(world).simulation_time end function register( @@ -362,90 +357,21 @@ function register( suggested_aid::Union{String,Nothing}=nothing; kwargs..., ) - actual_aid::String = "$AGENT_PREFIX$(world.agent_counter)" - if !isnothing(suggested_aid) && !haskey(world.agents, suggested_aid) - actual_aid = suggested_aid - end - world.agents[actual_aid] = agent - agent.aid = actual_aid - agent.context = AgentContext(world) - world.agent_counter += 1 - + agent = register(world.container, agent, suggested_aid, kwargs...) if !isnothing(world.task_sim) agent.scheduler = create_agent_scheduler(world.task_sim) end - return agent end -function process_message(world::World, msg::Any, meta::AbstractDict) - receiver_id = meta[RECEIVER_ID] - - if !haskey(world.agents, meta[RECEIVER_ID]) - @warn "Container $(keys(world.agents)) has no agent with id: $receiver_id" msg meta - else - agent = world.agents[receiver_id] - return dispatch_message(agent, msg, meta) - end -end - -struct NonWaitable end -function Base.wait(waitable::NonWaitable) end - -function forward_message(world::World, msg::Any, meta::AbstractDict) - push!(world.message_queue, MessageData(msg, meta, time(world))) - return NonWaitable() -end - -function send_message( - world::World, - 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(world, content, meta) -end - -""" - Base.getindex(world::World, index::String) - -Return the agent indexed by `index` (aid). -""" function Base.getindex(world::World, index::String) - return world.agents[index] -end -function Base.getindex(world::World, index::Int) - return agents(world)[index] -end - -function env(world::World) - return env(world.env) + return world.container[index] end -function space(world::World) - return space(world.env) +function Base.getindex(world::World, index::Int) + return world.container[index] end -function clock(world::World) - return world.clock +function shutdown(world::World) + shutdown(world.container) end - -function time(world::World) - return clock(world).simulation_time -end \ No newline at end of file diff --git a/src/world/topology.jl b/src/util/topology.jl similarity index 100% rename from src/world/topology.jl rename to src/util/topology.jl diff --git a/src/util/visualization.jl b/src/util/visualization.jl new file mode 100644 index 00000000..6bb95c6e --- /dev/null +++ b/src/util/visualization.jl @@ -0,0 +1,3 @@ + + +function visualize() end \ No newline at end of file diff --git a/test/environment_api_tests.jl b/test/environment_api_tests.jl index a09e76d1..2549c952 100644 --- a/test/environment_api_tests.jl +++ b/test/environment_api_tests.jl @@ -25,17 +25,17 @@ function Mango.on_step(behavior::TestBehavior, environment::Environment, clock:: end @testset "TestAgentWorldEvent" begin - container = create_world(DateTime(Millisecond(23)), + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0), behavior=TestBehavior()) agent1 = WorldEventAgent(0) - register(container, agent1) - agent2 = add_agent_composed_of(container, WorldEventRole(1)) + register(world, agent1) + agent2 = add_agent_composed_of(world, WorldEventRole(1)) - stepping_result = step_simulation(container) + stepping_result = step_simulation(world) @test agent1.counter == 14 @test agent2[WorldEventRole].counter == 15 - stepping_result = step_simulation(container) + stepping_result = step_simulation(world) @test agent1.counter == 28 @test agent2[WorldEventRole].counter == 29 end \ No newline at end of file diff --git a/test/express_api_tests.jl b/test/express_api_tests.jl index e8a6379d..78dbb10d 100644 --- a/test/express_api_tests.jl +++ b/test/express_api_tests.jl @@ -180,7 +180,7 @@ end 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/runtests.jl b/test/runtests.jl index 6e9d90fb..897d86a3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,7 +8,7 @@ using Documenter 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") diff --git a/test/simulation_container_tests.jl b/test/world_tests.jl similarity index 57% rename from test/simulation_container_tests.jl rename to test/world_tests.jl index c8fcc953..b3414c0b 100644 --- a/test/simulation_container_tests.jl +++ b/test/world_tests.jl @@ -19,40 +19,38 @@ end @testset "WorldKwargs" begin - container = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) agent1 = SimAgent(0) agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2) + register(world, agent1) + register(world, agent2) - send_message(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid), test=2) + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid), test=2) - stepping_result = step_simulation(container, 1) - - shutdown(container) + stepping_result = step_simulation(world, 1) @test agent1.counter == 11 end @testset "WorldNoProtocolSpecificAddr" begin - container = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) - @test isnothing(protocol_addr(container)) + @test isnothing(protocol_addr(world)) end @testset "WorldNoValidTargetCustomAid" begin - container = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) agent1 = SimAgent(0) agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2, "a1") + register(world, agent1) + register(world, agent2, "a1") - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid="abc")) + send_message(world.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) + @test_logs (:warn, "Container $(keys(world.container.agents)) has no agent with id: abc") min_level = Logging.Warn begin + stepping_result = step_simulation(world, 1) end @test agent1.counter == 0 @@ -62,41 +60,42 @@ end @testset "SimpleInternalSimulationWithoutDelayContainerTest" begin - container = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=0)) agent1 = SimAgent(0) agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2) + register(world, agent1) + register(world, 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)) + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - stepping_result = step_simulation(container, 1) + stepping_result = step_simulation(world, 1) - shutdown(container) @test agent1.counter == 10 @test agent2.counter == 10 - @test container.shutdown + + shutdown(world.container) + @test world.container.shutdown end @testset "SimpleInternalSimulationDelayGreaterStepSize" begin - container = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) agent1 = SimAgent(0) agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2) + register(world, agent1) + register(world, 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)) + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - stepping_result = step_simulation(container, 1) + stepping_result = step_simulation(world, 1) @test agent1.counter == 0 @test agent2.counter == 0 - stepping_result = step_simulation(container, 1) + stepping_result = step_simulation(world, 1) @test agent1.counter == 10 @test agent2.counter == 10 @@ -104,29 +103,29 @@ end @testset "SimpleInternalSimulationDelayMixedGreaterStepSize" begin - container = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) + world = create_world(DateTime(Millisecond(23)), communication_sim=SimpleCommunicationSimulation(default_delay_s=2)) agent1 = SimAgent(0) agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2) + register(world, agent1) + register(world, 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)) + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - stepping_result = step_simulation(container, 1) + stepping_result = step_simulation(world, 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)) + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - stepping_result = step_simulation(container, 1) + stepping_result = step_simulation(world, 1) @test agent1.counter == 10 @test agent2.counter == 10 - stepping_result = step_simulation(container, 1) + stepping_result = step_simulation(world, 1) @test agent1.counter == 20 @test agent2.counter == 20 @@ -135,23 +134,23 @@ end @testset "SimpleInternalSimulationLinkSpecificDelay" begin com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_world(DateTime(Millisecond(0)), communication_sim=com_sim) + world = create_world(DateTime(Millisecond(0)), communication_sim=com_sim) agent1 = SimAgent(0) agent2 = SimAgent(0) - register(container, agent1) - register(container, agent2) + 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(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - stepping_result = step_simulation(container, 1) + stepping_result = step_simulation(world, 1) @test agent1.counter == 10 @test agent2.counter == 0 - stepping_result = step_simulation(container, 1) + stepping_result = step_simulation(world, 1) @test agent1.counter == 10 @test agent2.counter == 10 @@ -170,11 +169,11 @@ end @testset "SimulationWithSpecificDelaysAndScheduledTasks" begin com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_world(DateTime(0), communication_sim=com_sim) + world = create_world(DateTime(0), communication_sim=com_sim) agent1 = SimSchedulingAgent(0, 0) agent2 = SimSchedulingAgent(0, 0) - register(container, agent1) - register(container, agent2) + 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 @@ -184,16 +183,16 @@ 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)) + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - stepping_result = step_simulation(container, 1) + stepping_result = step_simulation(world, 1) @test agent1.counter == 1 @test agent1.scheduled_counter == 111 @test agent2.counter == 0 - stepping_result = step_simulation(container, 1) + stepping_result = step_simulation(world, 1) @test agent1.counter == 1 @test agent1.scheduled_counter == 121 @@ -215,28 +214,28 @@ end @testset "SimulationWithSpecificDelaysAndScheduledTasksOnHandle" begin com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_world(DateTime(0), communication_sim=com_sim) + world = create_world(DateTime(0), communication_sim=com_sim) agent1 = ComplexSimSchedulingAgent(0, 0) agent2 = ComplexSimSchedulingAgent(0, 0) - register(container, agent1) - register(container, agent2) + 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(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - stepping_result = step_simulation(container, 1) + 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(container, 1) + stepping_result = step_simulation(world, 1) @test agent1.counter == 1 @test agent1.scheduled_counter == 121 @@ -262,28 +261,28 @@ end @testset "SimulationWithSpecificDelaysWithReplyOnHandle" begin com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_world(DateTime(0), communication_sim=com_sim) + world = create_world(DateTime(0), communication_sim=com_sim) agent1 = MoreComplexSimSchedulingAgent(0, 0) agent2 = MoreComplexSimSchedulingAgent(0, 0) - register(container, agent1) - register(container, agent2) + 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(container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid), agent2.aid) - send_message(container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid), agent2.aid) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - stepping_result = step_simulation(container, 1) + 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(container, 1) + stepping_result = step_simulation(world, 1) @test agent1.counter == 1 @test agent1.scheduled_counter == 121 @@ -294,11 +293,11 @@ end @testset "SimulationWithSpecificDelaysWithReplyOnHandleDiscreteEvent" begin com_sim = SimpleCommunicationSimulation(default_delay_s=0) - container = create_world(DateTime(0), communication_sim=com_sim) + world = create_world(DateTime(0), communication_sim=com_sim) agent1 = MoreComplexSimSchedulingAgent(0, 0) agent2 = MoreComplexSimSchedulingAgent(0, 0) - register(container, agent1) - register(container, agent2) + 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 @@ -308,10 +307,10 @@ 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)) + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid), agent2.aid) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) - stepping_result = step_simulation(container) + stepping_result = step_simulation(world) @test stepping_result.simulation_step_size_s == 0 @test agent1.counter == 0 @@ -319,7 +318,7 @@ end @test agent2.counter == 0 @test agent2.scheduled_counter == 0 - stepping_result = step_simulation(container) + stepping_result = step_simulation(world) @test stepping_result.simulation_step_size_s == 1 @test agent1.counter == 1 @@ -327,7 +326,7 @@ end @test agent2.counter == 1 @test agent2.scheduled_counter == 100 - stepping_result = step_simulation(container) + stepping_result = step_simulation(world) @test stepping_result.simulation_step_size_s == 1 @test agent1.counter == 1 @@ -335,7 +334,7 @@ end @test agent2.counter == 2 @test agent2.scheduled_counter == 200 - stepping_result = step_simulation(container) + stepping_result = step_simulation(world) @test isnothing(stepping_result) @@ -346,7 +345,7 @@ end # nothing end - stepping_result = step_simulation(container) + stepping_result = step_simulation(world) @test stepping_result.simulation_step_size_s == 0 @test agent1.counter == 1 @@ -354,7 +353,7 @@ end @test agent2.counter == 2 @test agent2.scheduled_counter == 200 - stepping_result = step_simulation(container) + stepping_result = step_simulation(world) @test stepping_result.simulation_step_size_s == 0.1 @test agent1.counter == 1 @@ -380,16 +379,16 @@ end end @testset "WorldAgentsAreOrdered" begin - container = create_world(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 + 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 \ No newline at end of file From c618b03230c17970fb7ca7b098260bf020a90a5a Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Fri, 20 Dec 2024 14:41:32 +0100 Subject: [PATCH 21/54] Scheduling, support awaitable for simulation feature. --- src/util/scheduling.jl | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/util/scheduling.jl b/src/util/scheduling.jl index c08f6bb2..e0cd8005 100644 --- a/src/util/scheduling.jl +++ b/src/util/scheduling.jl @@ -174,6 +174,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`. @@ -200,7 +209,7 @@ function execute_task(f::Function, scheduler::AbstractScheduler, data::DateTimeT end function execute_task(f::Function, scheduler::AbstractScheduler, data::AwaitableTaskData) - wait(data.awaitable) + wait(scheduler, data) f() end @@ -391,6 +400,11 @@ 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 tasks(scheduler::SimulationScheduler) return scheduler.tasks end From 486ff82b433a1447fa26bbf8b6656cf78e17f7e2 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Wed, 8 Jan 2025 23:14:45 +0100 Subject: [PATCH 22/54] Export behavior. --- src/environment/core.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/environment/core.jl b/src/environment/core.jl index f1139945..05adebac 100644 --- a/src/environment/core.jl +++ b/src/environment/core.jl @@ -1,6 +1,6 @@ export Environment, Space, Position, Position2D, Area2D, location, move, initialize, initialized, Behavior, schedule, WorldObserver, - emit_global_event, env + emit_global_event, behavior abstract type Position end abstract type Space{P<:Position} end @@ -13,7 +13,6 @@ end struct NoBehavior <: Behavior end - struct Position2D <: Position x::Real y::Real From 7401ae913f4dc041a235ebed8a10f4581842dc67 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Fri, 17 Jan 2025 22:44:58 +0100 Subject: [PATCH 23/54] Topology plotting. --- Project.toml | 10 ++-- docs/src/scheduling.md | 2 + src/Mango.jl | 1 - src/agent/core.jl | 12 ++++- src/container/mqtt.jl | 2 +- src/container/tcp.jl | 4 +- src/simulation/tasks.jl | 104 ++++++++++++++++++++++---------------- src/simulation/world.jl | 28 ++++++++-- src/util/scheduling.jl | 17 ++++++- src/util/topology.jl | 63 ++++++++++++++++++++++- src/util/visualization.jl | 3 -- test/agent_tests.jl | 7 +++ test/scheduler_tests.jl | 12 +++++ test/topology_tests.jl | 18 +++++++ 14 files changed, 222 insertions(+), 61 deletions(-) delete mode 100644 src/util/visualization.jl diff --git a/Project.toml b/Project.toml index 2a3a161a..1cf69ffb 100644 --- a/Project.toml +++ b/Project.toml @@ -5,12 +5,14 @@ repo = "https://github.com/OFFIS-DAI/Mango.jl" version = "0.4.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" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +Karnak = "cd156443-31ad-4f6f-850f-a93ee5f75905" LightBSON = "a4a7f996-b3a6-4de6-b9db-2fa5f350df41" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" @@ -21,20 +23,22 @@ Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] +Colors = "0.12.11" ConcurrentCollections = "~0.1" ConcurrentUtilities = "~2.2" +Graphs = "~1.10" JSON = "~0.21" +Karnak = "~1.1" LightBSON = "~0.2" +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" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test", "Documenter"] 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/src/Mango.jl b/src/Mango.jl index 9aa6a19f..cbf9fade 100644 --- a/src/Mango.jl +++ b/src/Mango.jl @@ -25,7 +25,6 @@ include("simulation/container.jl") include("environment/core.jl") include("simulation/world.jl") include("util/topology.jl") -include("util/visualization.jl") include("express/api.jl") diff --git a/src/agent/core.jl b/src/agent/core.jl index 0f7cb5c2..432f5bcf 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -16,7 +16,8 @@ export @agent, service_of_type, add_service!, services, - on_global_event + on_global_event, + sender_address using UUIDs @@ -171,6 +172,15 @@ function dispatch_message(agent::Agent, message::Any, meta::AbstractDict) 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 + """ handle_message(agent::Agent, message::Any, meta::Any) 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/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/simulation/tasks.jl b/src/simulation/tasks.jl index eee25be9..0d24d961 100644 --- a/src/simulation/tasks.jl +++ b/src/simulation/tasks.jl @@ -94,6 +94,61 @@ 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 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 + + # rethrow exception if exists + if istaskfailed(task) + Base.show_backtrace(stderr, 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 @@ -109,49 +164,12 @@ function step_iteration(task_sim::SimpleTaskSimulation, step_size_s::Real, first 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 + bt = stacktrace(catch_backtrace()) + showerror(stderr, ex, bt) + rethrow(ex) end end end diff --git a/src/simulation/world.jl b/src/simulation/world.jl index 688bc887..9dd27e43 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -197,7 +197,13 @@ function cs_step_iteration(world::World, 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 - @spawnlog process_message(world.container, mp.content[1], mp.content[2]) + Threads.@spawn try + process_message(world.container, mp.content[1], mp.content[2]) + catch ex + bt = stacktrace(catch_backtrace()) + showerror(stderr, ex, bt) + rethrow(ex) + end else # process it later push!(messages(world.container), MessageData(mp.content[1], mp.content[2], mp.sent_date)) @@ -289,8 +295,22 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ task_iter_result = nothing comm_iter_result = nothing @sync begin - Threads.@spawn comm_iter_result = cs_step_iteration(world, time_step_s, first_step ? comm_result : nothing) - Threads.@spawn task_iter_result = step_iteration(world.task_sim, time_step_s, first_step) + + Threads.@spawn try + comm_iter_result = cs_step_iteration(world, time_step_s, first_step ? comm_result : nothing) + catch ex + bt = stacktrace(catch_backtrace()) + showerror(stderr, ex, bt) + rethrow(ex) + end + + Threads.@spawn try + task_iter_result = step_iteration(world.task_sim, time_step_s, first_step) + catch ex + bt = stacktrace(catch_backtrace()) + showerror(stderr, ex, bt) + rethrow(ex) + end end first_step = false push!(task_sim_result.results, task_iter_result) @@ -336,7 +356,7 @@ struct NonWaitable end function Base.wait(waitable::NonWaitable) end function env(world::World) - return env(world.env) + return world.env end function space(world::World) diff --git a/src/util/scheduling.jl b/src/util/scheduling.jl index e0cd8005..feebcca5 100644 --- a/src/util/scheduling.jl +++ b/src/util/scheduling.jl @@ -1,6 +1,7 @@ export TaskData, PeriodicTaskData, InstantTaskData, + DelayTaskData, DateTimeTaskData, AwaitableTaskData, ConditionalTaskData, @@ -159,6 +160,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. """ @@ -203,6 +211,11 @@ 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 - now(scheduler)).value / 1000) f() @@ -228,7 +241,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 @@ -416,7 +429,7 @@ function schedule(f::Function, scheduler::SimulationScheduler, data::TaskData) end function do_schedule(f::Function, scheduler::SimulationScheduler, data::TaskData, event::Base.Event) - task = Threads.@spawn execute_task(f, scheduler, data) + task = @spawnlog 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 index d760aab2..af274639 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -1,9 +1,15 @@ -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! +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!, + plot using MetaGraphsNext using Graphs import Graphs.add_edge! +using Karnak +using Colors + @kwdef struct Node id::Int agents::Vector{Agent} = Vector() @@ -313,4 +319,59 @@ end function Graphs.nv(topology::Topology) return nv(topology.graph) +end + +function plot(topology::Topology; + annotate_aids::Bool=false, + write_to::Union{Nothing,String}=nothing, + dimensions::Tuple{Int,Int}=(800, 600)) + + g = topology.graph + node_id_map = collect(labels(topology.graph)) + + nodecolor = "firebrick" + textcolor = "white" + edgecolor = colorant"lightgray" + + @drawsvg begin + background("grey10") + sethue(nodecolor) + drawgraph(g, layout=stress, + margin=50, + edgegaps=30, + edgestrokeweights=2, + vertexshapes=:circle, + vertexshapesizes=30, + vertexlabels=(v) -> "n$(node_id_map[v]) ($(length(topology.graph[node_id_map[v]].agents)))", + vertexfunction=(v, c) -> begin + @layer begin + sethue(nodecolor) + circle(c[v], 25, :fill) + sethue(textcolor) + t_p = c[v] + (0, 10) + translate(t_p) + label("n$(node_id_map[v]) ($(length(topology.graph[node_id_map[v]].agents)))") + if annotate_aids + translate(Point(50, 0)) + label(string([aid(a) for a in topology.graph[node_id_map[v]].agents])) + end + end + end, + edgestrokecolors=edgecolor, + edgelabelcolors=edgecolor, + edgelabels=(n, s, d, f, t) -> begin + θ = slope(f, t) + fontsize(12) + translate(midpoint(f, t)) + rotate(θ) + sethue(textcolor) + label("state $(g[node_id_map[s],node_id_map[d]])", offset=-15) + end) + end dimensions[1] dimensions[2] + + svg = svgstring() + if !isnothing(write_to) + write(write_to, svg) + end + return svg end \ No newline at end of file diff --git a/src/util/visualization.jl b/src/util/visualization.jl deleted file mode 100644 index 6bb95c6e..00000000 --- a/src/util/visualization.jl +++ /dev/null @@ -1,3 +0,0 @@ - - -function visualize() end \ No newline at end of file diff --git a/test/agent_tests.jl b/test/agent_tests.jl index 1873d291..c4d498d1 100644 --- a/test/agent_tests.jl +++ b/test/agent_tests.jl @@ -333,4 +333,11 @@ 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 \ No newline at end of file diff --git a/test/scheduler_tests.jl b/test/scheduler_tests.jl index 77745b9b..104e3d05 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 diff --git a/test/topology_tests.jl b/test/topology_tests.jl index 048de7bc..9e1db476 100644 --- a/test/topology_tests.jl +++ b/test/topology_tests.jl @@ -222,4 +222,22 @@ end @test ne(topology) == (n_nodes^2 - n_nodes) / 2 @test nv(topology) == 5 @test collect(vertices(topology)) == [1, 2, 3, 4, 5] +end + +@testset "TestTopologyPlotting" begin + topology = create_topology() do topology + n1 = add_node!(topology, TopologyAgent()) + n2 = add_node!(topology, TopologyAgent()) + n3 = add_node!(topology, TopologyAgent(), id=12) + add_edge!(topology, n1, n2) + add_edge!(topology, n1, n3) + end + + svg_string = plot(topology, write_to="test_topology_plot.svg") + svg_string2 = plot(topology, annotate_aids=true) + + @test length(svg_string) == 25810 + @test length(svg_string2) == 32530 + + rm("test_topology_plot.svg") end \ No newline at end of file From ccfa02140c851f2dad44bae5935fff78366da813 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Fri, 17 Jan 2025 22:45:27 +0100 Subject: [PATCH 24/54] Topology plotting. --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 1cf69ffb..9f98a7af 100644 --- a/Project.toml +++ b/Project.toml @@ -23,7 +23,7 @@ Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] -Colors = "0.12.11" +Colors = "~0.12" ConcurrentCollections = "~0.1" ConcurrentUtilities = "~2.2" Graphs = "~1.10" From 90d97c71d1d6970e7b3fa0c64e8b9ce402fb1c55 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Fri, 17 Jan 2025 23:45:03 +0100 Subject: [PATCH 25/54] Relax topology test. --- test/topology_tests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/topology_tests.jl b/test/topology_tests.jl index 9e1db476..328e6f49 100644 --- a/test/topology_tests.jl +++ b/test/topology_tests.jl @@ -236,8 +236,8 @@ end svg_string = plot(topology, write_to="test_topology_plot.svg") svg_string2 = plot(topology, annotate_aids=true) - @test length(svg_string) == 25810 - @test length(svg_string2) == 32530 + @test length(svg_string) > 10000 + @test length(svg_string2) > 20000 rm("test_topology_plot.svg") end \ No newline at end of file From e7c1d64592da261b6bce4fb9ac5b9e584773ec21 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Sat, 18 Jan 2025 00:13:03 +0100 Subject: [PATCH 26/54] More Tests. --- test/environment_api_tests.jl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/environment_api_tests.jl b/test/environment_api_tests.jl index 2549c952..35119bdd 100644 --- a/test/environment_api_tests.jl +++ b/test/environment_api_tests.jl @@ -38,4 +38,15 @@ end 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 \ No newline at end of file From e264ce03458098023710e869e99e6d12d18710e3 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Mon, 27 Jan 2025 00:13:23 +0100 Subject: [PATCH 27/54] Adding visualization for communication (with slider), fixing some world bugs. --- Project.toml | 6 + src/Mango.jl | 2 + src/environment/core.jl | 17 +-- src/simulation/communication.jl | 2 +- src/simulation/container.jl | 8 +- src/simulation/world.jl | 171 +++++++++++++++++++++++++-- src/util/topology.jl | 74 +++--------- src/visualization/communication.jl | 184 +++++++++++++++++++++++++++++ src/visualization/observation.jl | 70 +++++++++++ test/environment_api_tests.jl | 4 +- test/runtests.jl | 1 + test/topology_tests.jl | 18 --- test/visualization_tests.jl | 72 +++++++++++ 13 files changed, 524 insertions(+), 105 deletions(-) create mode 100644 src/visualization/communication.jl create mode 100644 src/visualization/observation.jl create mode 100644 test/visualization_tests.jl diff --git a/Project.toml b/Project.toml index 9f98a7af..26228e35 100644 --- a/Project.toml +++ b/Project.toml @@ -5,11 +5,14 @@ repo = "https://github.com/OFFIS-DAI/Mango.jl" version = "0.4.0" [deps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" 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" +GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" +GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" Karnak = "cd156443-31ad-4f6f-850f-a93ee5f75905" @@ -23,9 +26,12 @@ Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] +CairoMakie = "0.13.1" Colors = "~0.12" ConcurrentCollections = "~0.1" ConcurrentUtilities = "~2.2" +GLMakie = "0.11.2" +GraphMakie = "0.5.13" Graphs = "~1.10" JSON = "~0.21" Karnak = "~1.1" diff --git a/src/Mango.jl b/src/Mango.jl index cbf9fade..77914e3d 100644 --- a/src/Mango.jl +++ b/src/Mango.jl @@ -25,6 +25,8 @@ include("simulation/container.jl") include("environment/core.jl") include("simulation/world.jl") include("util/topology.jl") +include("visualization/communication.jl") +include("visualization/observation.jl") include("express/api.jl") diff --git a/src/environment/core.jl b/src/environment/core.jl index 05adebac..0fe14c88 100644 --- a/src/environment/core.jl +++ b/src/environment/core.jl @@ -36,7 +36,6 @@ with the environment and exist in the defined space. space::S = Area2D(width=10, height=10) behavior::Behavior = NoBehavior() observers::Vector{WorldObserver} = Vector{WorldObserver}() - data_selectors::Vector{Function} = Vector{Function}() initialized::Bool = false end @@ -92,8 +91,14 @@ function initialize(space::Area2D, agents::Vector{A}) where {A<:Agent} end end +function initialize(behavior::Behavior) + # default no initialization +end + function initialize(environment::Environment{S}, agents::Vector{A}) where {S<:Space} where {A<:Agent} initialize(environment.space, agents) + initialize(behavior(environment)) + environment.initialized = true end """ @@ -136,14 +141,4 @@ function emit_global_event(environment::Environment, event::Any) for observer in environment.observers dispatch_global_event(observer, event) end -end - -""" - select(environment::Environment, selector::Function) - -Select an output attribute, which will be recorded while -the simulation is running (every step!). -""" -function select!(environment::Environment, selector::Function) - push!(environment.data_selectors, selector) end \ No newline at end of file diff --git a/src/simulation/communication.jl b/src/simulation/communication.jl index 4fd971ce..c1dc51e9 100644 --- a/src/simulation/communication.jl +++ b/src/simulation/communication.jl @@ -12,7 +12,7 @@ Package result """ struct PackageResult reached::Bool - delay_s::UInt64 + delay_s::Real end """ diff --git a/src/simulation/container.jl b/src/simulation/container.jl index f2f0bc72..e57f13e8 100644 --- a/src/simulation/container.jl +++ b/src/simulation/container.jl @@ -11,6 +11,7 @@ end @kwdef mutable struct SimulationContainer <: ContainerInterface clock::Clock + current_step_size_s::Real = 0 agents::OrderedDict{String,Agent} = OrderedDict{String,Agent}() agent_counter::Integer = 0 shutdown::Bool = false @@ -44,7 +45,12 @@ function register( end function forward_message(container::SimulationContainer, msg::Any, meta::AbstractDict) - push!(container.message_queue, MessageData(msg, meta, time(container))) + push!(container.message_queue, + MessageData(msg, + meta, + add_seconds(time(container), container.current_step_size_s) + ) + ) return NonWaitable() end diff --git a/src/simulation/world.jl b/src/simulation/world.jl index 9dd27e43..ca178e77 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -1,6 +1,7 @@ export World, register, send_message, shutdown, protocol_addr, create_world, step_simulation, SimulationResult, CommunicationSimulationResult, - TaskSimulationResult, on_step, discrete_step_until, env, space, time, clock + TaskSimulationResult, on_step, discrete_step_until, env, space, time, clock, + record_world!, record_agent! using Base.Threads using Dates @@ -37,6 +38,7 @@ function create_world(start_time::DateTime; world = World() world.clock.simulation_time = start_time + world.initial_time = start_time if !isnothing(communication_sim) world.communication_sim = communication_sim end @@ -64,16 +66,47 @@ function dispatch_global_event(observer::DispatchToAgentWorldObserver, event::An 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 +end + +""" +A 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 +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)) + initial_time::DateTime = DateTime(0) container::SimulationContainer = SimulationContainer(clock=clock) env::Environment = Environment(scheduler=SimulationScheduler(clock=clock)) task_sim::TaskSimulation = SimpleTaskSimulation(clock=clock) communication_sim::CommunicationSimulation = SimpleCommunicationSimulation() world_observer::WorldObserver = DispatchToAgentWorldObserver(container.agents) + data_collections::Dict{String,WorldRecording} = Dict() + data_agent_collections::Dict{String,AgentsRecording} = Dict() + data_collectors::Vector{Function} = Vector() + recorded_messages::Vector{MessageTransaction} = Vector() end function agents(world::World)::Vector{Agent} @@ -197,6 +230,11 @@ function cs_step_iteration(world::World, 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_aid, + mp.receiver_aid, + 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 @@ -246,6 +284,37 @@ function determine_time_step(world::World) 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, (time(world) - world.initial_time).value / 1000) + 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() + 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 + """ step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{SimulationResult,Nothing} @@ -258,6 +327,7 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ # 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 @@ -269,13 +339,6 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ first_step = true time_step_s = step_size_s - 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 - # 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 @@ -284,9 +347,15 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ time_step_s, comm_result = determine_time_step(world) @debug "Determined the size to be $time_step_s" if isnothing(time_step_s) + # only step guaranteed entities + step_all_entities(world, 0) return nothing end end + world.container.current_step_size_s = time_step_s + + step_all_entities(world, time_step_s) + elapsed = @elapsed begin # now we process everything which happened in the steps, # tasks and previous iterations @@ -322,9 +391,12 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ @debug "The simulation step needed $elapsed seconds" world.clock.simulation_time = add_seconds(time(world), time_step_s) + world.container.current_step_size_s = 0 @debug "New time" time(world) + do_recordings(world) + return SimulationResult(elapsed, messaging_sim_result, task_sim_result, time_step_s) end @@ -342,13 +414,16 @@ function discrete_step_until(world::World, max_advance_time_s::Real) prev_time = nothing results = [] - while isnothing(prev_time) || ((prev_time < time(world) || length(results) == 1) - && - initial_time + Second(max_advance_time_s) > time(world)) + elapsed = @elapsed begin + while isnothing(prev_time) || ((prev_time < time(world) || length(results) == 1) + && + add_seconds(initial_time, max_advance_time_s) > time(world)) - prev_time = time(world) - push!(results, step_simulation(world)) + prev_time = time(world) + push!(results, step_simulation(world)) + end end + @info "The discrete event simulation needed $elapsed seconds" return results end @@ -384,6 +459,76 @@ function register( 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) + return get!(world.data_collections, key, WorldRecording()) +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) + return get!(world.data_agent_collections, key, AgentsRecording()) +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) + push!(world.data_collectors, () -> collector(world, data_collection(world, key))) +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) + dac = data_agent_collection(world, key) + for agent in values(agents(world)) + push!(world.data_collectors, () -> collector(world, agent, dac)) + end + push!(world.data_collectors, () -> push!(dac.time, (time(world) - world.initial_time).value / 1000)) +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) + collect_data(world, key) 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) + collect_agent_data(world, key) do w, a, dc + insert_agent_recording!(dc, w, a, agent_recorder(a)) + end +end + function Base.getindex(world::World, index::String) return world.container[index] end diff --git a/src/util/topology.jl b/src/util/topology.jl index af274639..c2b2ab0c 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -1,15 +1,12 @@ 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!, - plot + auto_assign! using MetaGraphsNext using Graphs import Graphs.add_edge! -using Karnak -using Colors - @kwdef struct Node id::Int agents::Vector{Agent} = Vector() @@ -225,6 +222,20 @@ function per_node(assign_runnable::Function, topology::Topology) _build_neighborhoods_and_inject(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) + for (i, label) in enumerate(labels(topology.graph)) + node = topology.graph[label] + add!(node, container[i]) + end + _build_neighborhoods_and_inject(topology) +end + """ add!(node, agent::Agent...) @@ -320,58 +331,3 @@ end function Graphs.nv(topology::Topology) return nv(topology.graph) end - -function plot(topology::Topology; - annotate_aids::Bool=false, - write_to::Union{Nothing,String}=nothing, - dimensions::Tuple{Int,Int}=(800, 600)) - - g = topology.graph - node_id_map = collect(labels(topology.graph)) - - nodecolor = "firebrick" - textcolor = "white" - edgecolor = colorant"lightgray" - - @drawsvg begin - background("grey10") - sethue(nodecolor) - drawgraph(g, layout=stress, - margin=50, - edgegaps=30, - edgestrokeweights=2, - vertexshapes=:circle, - vertexshapesizes=30, - vertexlabels=(v) -> "n$(node_id_map[v]) ($(length(topology.graph[node_id_map[v]].agents)))", - vertexfunction=(v, c) -> begin - @layer begin - sethue(nodecolor) - circle(c[v], 25, :fill) - sethue(textcolor) - t_p = c[v] + (0, 10) - translate(t_p) - label("n$(node_id_map[v]) ($(length(topology.graph[node_id_map[v]].agents)))") - if annotate_aids - translate(Point(50, 0)) - label(string([aid(a) for a in topology.graph[node_id_map[v]].agents])) - end - end - end, - edgestrokecolors=edgecolor, - edgelabelcolors=edgecolor, - edgelabels=(n, s, d, f, t) -> begin - θ = slope(f, t) - fontsize(12) - translate(midpoint(f, t)) - rotate(θ) - sethue(textcolor) - label("state $(g[node_id_map[s],node_id_map[d]])", offset=-15) - end) - end dimensions[1] dimensions[2] - - svg = svgstring() - if !isnothing(write_to) - write(write_to, svg) - end - return svg -end \ No newline at end of file diff --git a/src/visualization/communication.jl b/src/visualization/communication.jl new file mode 100644 index 00000000..8505bb8e --- /dev/null +++ b/src/visualization/communication.jl @@ -0,0 +1,184 @@ +export plot + +using Karnak +using Colors +using GLMakie +using GraphMakie.NetworkLayout +using GraphMakie +using Graphs +using Dates + +function plot(topology::Topology; + annotate_aids::Bool=false, + write_to::Union{Nothing,String}=nothing, + dimensions::Tuple{Int,Int}=(800, 600)) + + g = topology.graph + node_id_map = collect(labels(topology.graph)) + + nodecolor = "firebrick" + textcolor = "white" + edgecolor = colorant"lightgray" + + @drawsvg begin + background("grey10") + sethue(nodecolor) + drawgraph(g, layout=shell, + margin=50, + edgegaps=30, + edgestrokeweights=2, + vertexshapes=:circle, + vertexshapesizes=30, + vertexlabels=(v) -> "n$(node_id_map[v]) ($(length(topology.graph[node_id_map[v]].agents)))", + vertexfunction=(v, c) -> begin + @layer begin + sethue(nodecolor) + circle(c[v], 25, :fill) + sethue(textcolor) + t_p = c[v] + (0, 10) + translate(t_p) + label("n$(node_id_map[v]) ($(length(topology.graph[node_id_map[v]].agents)))") + if annotate_aids + translate(Karnak.Point(50, 0)) + label(string([aid(a) for a in topology.graph[node_id_map[v]].agents])) + end + end + end, + edgestrokecolors=edgecolor, + edgelabelcolors=edgecolor, + edgelabels=(n, s, d, f, t) -> begin + θ = slope(f, t) + fontsize(12) + translate(midpoint(f, t)) + rotate(θ) + sethue(textcolor) + label("state $(g[node_id_map[s],node_id_map[d]])", offset=-15) + end) + end dimensions[1] dimensions[2] + + svg = svgstring() + if !isnothing(write_to) + write(write_to, svg) + end + return svg +end + +function _to_seconds(date::DateTime, init::DateTime) + return (date - init).value / 1000 +end + +function _find_edge(graph::AbstractGraph, sender_id::String, receiver_id::String) + for (i, edge) in enumerate(edges(graph)) + if collect(labels(graph))[src(edge)] == parse(Int, sender_id) && collect(labels(graph))[dst(edge)] == parse(Int, receiver_id) + return i, edge, :sender + elseif collect(labels(graph))[dst(edge)] == parse(Int, sender_id) && collect(labels(graph))[src(edge)] == parse(Int, receiver_id) + return i, edge, :receiver + end + end + return -1 +end + +function show_communication_data(topology::Topology, + messages::Vector{MessageTransaction}; + resolution_s::Real=0.1) + + GLMakie.activate!() + + fig = Figure() + ax = Axis(fig[1, 1]) + + min_date = min([m.sent_date for m in messages]...) + max_date = max([m.arriving_date for m in messages]...) + + delta = _to_seconds(max_date, min_date) + sg = SliderGrid(fig[2, 1], + (label="Time", range=0:resolution_s:delta, format="{:.1f}", startvalue=0), + tellheight=true) + + sliderobservable = sg.sliders[1].value + + g = topology.graph + + edgecolors = lift(sliderobservable) do s + time = Int(floor(s)) + @info "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, message.sender_id, message.receiver_id) + @info found collect(labels(g)) + if found != -1 + edgecolors[found[1]] = :red + end + end + end + edgecolors + end + + elabels = lift(sliderobservable) do s + time = Int(floor(s)) + 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, message.sender_id, message.receiver_id) + if found != -1 + elabels[found[1]] = "$(typeof(message.content)): $(last("$(message.content)", 5))" + end + end + end + elabels + end + + arrow_markers = lift(sliderobservable) do s + time = Int(floor(s)) + 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, message.sender_id, message.receiver_id) + if found != -1 + markers[found[1]] = found[3] == :sender ? :rtriangle : :ltriangle + end + end + end + markers + end + + arrow_shifts = lift(sliderobservable) do s + time = Int(floor(s)) + 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, message.sender_id, message.receiver_id) + if found != -1 + shifts[found[1]] = 0.8 + end + end + end + shifts + end + + graphplot!(ax, g, layout=Shell(), + edge_color=edgecolors, + elabels=elabels, + arrow_show=true, + node_size=48, + node_color=:gray, + arrow_size=24, + arrow_shift=arrow_shifts, + arrow_marker=arrow_markers, + ilabels=repr.(1:nv(g)), + ilabels_color=:white) + + hidedecorations!(ax) + hidespines!(ax) + + fig +end \ No newline at end of file diff --git a/src/visualization/observation.jl b/src/visualization/observation.jl new file mode 100644 index 00000000..a1f8d52b --- /dev/null +++ b/src/visualization/observation.jl @@ -0,0 +1,70 @@ +export plot_world, plot_agents, plot_recordings + +using CairoMakie +using GLMakie + +function plot_world(world::World, recording::String; write_to::Union{Nothing,String}="world_observation.svg", fig=Figure()) + CairoMakie.activate!() + + data = data_collection(world, recording) + ax = Axis(fig, + title="$recording over time", + xlabel="time (seconds)", + ylabel=recording, + ) + lines!(ax, data.time, data.timeseries) + if !isnothing(write_to) + save(write_to, fig) + end +end + +function plot_agents(world::World, recording::String; write_to::Union{Nothing,String}="agent_observation.svg", fig=Figure()) + CairoMakie.activate!() + + data = data_agent_collection(world, recording) + ax = Axis(fig, + title="$recording over time for each agent", + xlabel="time (seconds)", + ylabel=recording, + ) + pairs = collect(data.timeseries) + labels = [pair[1] for pair in pairs] + values = [pair[2] for pair in pairs] + series!(ax, data.time, hcat(values...)', labels=labels, color=:coolwarm) + axislegend(ax, position=:lt) + if !isnothing(write_to) + save(write_to, fig) + end +end + +function _create_label(layout, label) + return Label(layout[1, 1, TopLeft()], label, + fontsize=26, + font=:bold, + padding=(0, 5, 5, 0), + halign=:left) +end + +function plot_recordings(world::World; write_to::Union{Nothing,String}="observation.svg", size=(600, 600)) + CairoMakie.activate!() + + dc = world.data_collections + dac = world.data_agent_collections + main_fig = Figure(size=size) + world_layout = main_fig[1, 1] = GridLayout() + agent_layout = main_fig[2, 1] = GridLayout() + + for (i, key) in enumerate(keys(dc)) + plot_world(world, key, write_to=nothing, fig=world_layout[1, i]) + end + _create_label(world_layout, "W") + + for (i, key) in enumerate(keys(dac)) + plot_agents(world, key, write_to=nothing, fig=agent_layout[1, i]) + end + _create_label(agent_layout, "A") + + if !isnothing(write_to) + save(write_to, main_fig) + end +end \ No newline at end of file diff --git a/test/environment_api_tests.jl b/test/environment_api_tests.jl index 35119bdd..35ef1576 100644 --- a/test/environment_api_tests.jl +++ b/test/environment_api_tests.jl @@ -33,8 +33,8 @@ end agent2 = add_agent_composed_of(world, WorldEventRole(1)) stepping_result = step_simulation(world) - @test agent1.counter == 14 - @test agent2[WorldEventRole].counter == 15 + @test agent1.counter == 7 + @test agent2[WorldEventRole].counter == 8 stepping_result = step_simulation(world) @test agent1.counter == 28 @test agent2[WorldEventRole].counter == 29 diff --git a/test/runtests.jl b/test/runtests.jl index 897d86a3..7ec58db4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,5 +15,6 @@ using Documenter include("express_api_tests.jl") include("topology_tests.jl") include("environment_api_tests.jl") + incude("visualization_tests.jl") doctest(Mango) end \ No newline at end of file diff --git a/test/topology_tests.jl b/test/topology_tests.jl index 328e6f49..048de7bc 100644 --- a/test/topology_tests.jl +++ b/test/topology_tests.jl @@ -222,22 +222,4 @@ end @test ne(topology) == (n_nodes^2 - n_nodes) / 2 @test nv(topology) == 5 @test collect(vertices(topology)) == [1, 2, 3, 4, 5] -end - -@testset "TestTopologyPlotting" begin - topology = create_topology() do topology - n1 = add_node!(topology, TopologyAgent()) - n2 = add_node!(topology, TopologyAgent()) - n3 = add_node!(topology, TopologyAgent(), id=12) - add_edge!(topology, n1, n2) - add_edge!(topology, n1, n3) - end - - svg_string = plot(topology, write_to="test_topology_plot.svg") - svg_string2 = plot(topology, annotate_aids=true) - - @test length(svg_string) > 10000 - @test length(svg_string2) > 20000 - - rm("test_topology_plot.svg") end \ No newline at end of file diff --git a/test/visualization_tests.jl b/test/visualization_tests.jl new file mode 100644 index 00000000..18ebceb0 --- /dev/null +++ b/test/visualization_tests.jl @@ -0,0 +1,72 @@ +using Mango +using Test +using Graphs +using Dates + +@agent struct TopologyPlotAgent +end + +@testset "TestTopologyPlotting" begin + topology = create_topology() do topology + n1 = add_node!(topology, TopologyPlotAgent()) + n2 = add_node!(topology, TopologyPlotAgent()) + n3 = add_node!(topology, TopologyPlotAgent(), id=12) + add_edge!(topology, n1, n2) + add_edge!(topology, n1, n3) + end + + svg_string = plot(topology, write_to="test_topology_plot.svg") + svg_string2 = plot(topology, annotate_aids=true) + + @test length(svg_string) > 10000 + @test length(svg_string2) > 20000 + + rm("test_topology_plot.svg") +end + +@agent struct MyVisuBehavingAgent + counter::Int + other_aid::String +end + +function Mango.on_step(agent::MyVisuBehavingAgent, environment::Environment, 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 From 85fff9e21ec661a267e6e6802efc1a096e85f759 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Mon, 27 Jan 2025 10:05:01 +0100 Subject: [PATCH 28/54] Tests for communication visualization. --- src/simulation/world.jl | 4 +- src/visualization/communication.jl | 60 ++++++++++++++++++------------ test/runtests.jl | 2 +- test/visualization_tests.jl | 17 +++++++++ 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/src/simulation/world.jl b/src/simulation/world.jl index ca178e77..c7b2cbc5 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -230,8 +230,8 @@ function cs_step_iteration(world::World, 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_aid, - mp.receiver_aid, + 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])) diff --git a/src/visualization/communication.jl b/src/visualization/communication.jl index 8505bb8e..b0d6a09c 100644 --- a/src/visualization/communication.jl +++ b/src/visualization/communication.jl @@ -1,4 +1,4 @@ -export plot +export plot, show_communication_data using Karnak using Colors @@ -67,11 +67,11 @@ function _to_seconds(date::DateTime, init::DateTime) return (date - init).value / 1000 end -function _find_edge(graph::AbstractGraph, sender_id::String, receiver_id::String) +function _find_edge(graph::AbstractGraph, sender_id::Int, receiver_id::Int) for (i, edge) in enumerate(edges(graph)) - if collect(labels(graph))[src(edge)] == parse(Int, sender_id) && collect(labels(graph))[dst(edge)] == parse(Int, receiver_id) + if collect(labels(graph))[src(edge)] == sender_id && collect(labels(graph))[dst(edge)] == receiver_id return i, edge, :sender - elseif collect(labels(graph))[dst(edge)] == parse(Int, sender_id) && collect(labels(graph))[src(edge)] == parse(Int, receiver_id) + elseif collect(labels(graph))[dst(edge)] == sender_id && collect(labels(graph))[src(edge)] == receiver_id return i, edge, :receiver end end @@ -79,15 +79,17 @@ function _find_edge(graph::AbstractGraph, sender_id::String, receiver_id::String end function show_communication_data(topology::Topology, - messages::Vector{MessageTransaction}; - resolution_s::Real=0.1) + messages::Vector{MessageTransaction}, + initial_time::DateTime=DateTime(0); + resolution_s::Real=0.1, + display::Bool=true) GLMakie.activate!() fig = Figure() ax = Axis(fig[1, 1]) - min_date = min([m.sent_date for m in messages]...) + min_date = initial_time max_date = max([m.arriving_date for m in messages]...) delta = _to_seconds(max_date, min_date) @@ -98,17 +100,21 @@ function show_communication_data(topology::Topology, sliderobservable = sg.sliders[1].value g = topology.graph - - edgecolors = lift(sliderobservable) do s - time = Int(floor(s)) - @info "time" + aid_to_node_id = Dict{String,Int}() + for label in labels(topology.graph) + node = topology.graph[label] + for agent in node.agents + aid_to_node_id[aid(agent)] = label + end + end + 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, message.sender_id, message.receiver_id) - @info found collect(labels(g)) + 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 @@ -117,14 +123,13 @@ function show_communication_data(topology::Topology, edgecolors end - elabels = lift(sliderobservable) do s - time = Int(floor(s)) + elabels = lift(sliderobservable) do time 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, message.sender_id, message.receiver_id) + found = _find_edge(g, aid_to_node_id[message.sender_id], aid_to_node_id[message.receiver_id]) if found != -1 elabels[found[1]] = "$(typeof(message.content)): $(last("$(message.content)", 5))" end @@ -133,14 +138,13 @@ function show_communication_data(topology::Topology, elabels end - arrow_markers = lift(sliderobservable) do s - time = Int(floor(s)) + 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, message.sender_id, message.receiver_id) + found = _find_edge(g, aid_to_node_id[message.sender_id], aid_to_node_id[message.receiver_id]) if found != -1 markers[found[1]] = found[3] == :sender ? :rtriangle : :ltriangle end @@ -149,14 +153,13 @@ function show_communication_data(topology::Topology, markers end - arrow_shifts = lift(sliderobservable) do s - time = Int(floor(s)) + 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, message.sender_id, message.receiver_id) + 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 @@ -180,5 +183,16 @@ function show_communication_data(topology::Topology, hidedecorations!(ax) hidespines!(ax) - fig + if display + wait(display(fig)) + end + return fig +end + +function show_communication_data(topology::Topology, + world::World; + resolution_s::Real=0.1, + display::Bool=true) + return show_communication_data(topology, world.recorded_messages, world.initial_time, + resolution_s=resolution_s, display=display) end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 7ec58db4..6c7faa99 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,6 +15,6 @@ using Documenter include("express_api_tests.jl") include("topology_tests.jl") include("environment_api_tests.jl") - incude("visualization_tests.jl") + include("visualization_tests.jl") doctest(Mango) end \ No newline at end of file diff --git a/test/visualization_tests.jl b/test/visualization_tests.jl index 18ebceb0..0d607a08 100644 --- a/test/visualization_tests.jl +++ b/test/visualization_tests.jl @@ -70,3 +70,20 @@ end 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(topology, world, display=false) +end \ No newline at end of file From 41b626737c43b8bc72b037cb9ba2096965478d85 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Mon, 27 Jan 2025 11:47:52 +0100 Subject: [PATCH 29/54] Clean up dependencies. --- Project.toml | 4 -- src/visualization/communication.jl | 60 ------------------------------ src/visualization/observation.jl | 5 +-- test/visualization_tests.jl | 18 --------- 4 files changed, 1 insertion(+), 86 deletions(-) diff --git a/Project.toml b/Project.toml index 26228e35..d82ae15b 100644 --- a/Project.toml +++ b/Project.toml @@ -11,11 +11,9 @@ ConcurrentCollections = "5060bff5-0b44-40c5-b522-fcd3ca5cecdd" ConcurrentUtilities = "f0e56b4a-5159-44fe-b623-3e5288b988bb" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" -GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -Karnak = "cd156443-31ad-4f6f-850f-a93ee5f75905" LightBSON = "a4a7f996-b3a6-4de6-b9db-2fa5f350df41" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" @@ -30,11 +28,9 @@ CairoMakie = "0.13.1" Colors = "~0.12" ConcurrentCollections = "~0.1" ConcurrentUtilities = "~2.2" -GLMakie = "0.11.2" GraphMakie = "0.5.13" Graphs = "~1.10" JSON = "~0.21" -Karnak = "~1.1" LightBSON = "~0.2" MetaGraphsNext = "~0.7" Mosquitto = "~0.10" diff --git a/src/visualization/communication.jl b/src/visualization/communication.jl index b0d6a09c..5e77f7fb 100644 --- a/src/visualization/communication.jl +++ b/src/visualization/communication.jl @@ -1,68 +1,10 @@ export plot, show_communication_data -using Karnak -using Colors -using GLMakie using GraphMakie.NetworkLayout using GraphMakie using Graphs using Dates -function plot(topology::Topology; - annotate_aids::Bool=false, - write_to::Union{Nothing,String}=nothing, - dimensions::Tuple{Int,Int}=(800, 600)) - - g = topology.graph - node_id_map = collect(labels(topology.graph)) - - nodecolor = "firebrick" - textcolor = "white" - edgecolor = colorant"lightgray" - - @drawsvg begin - background("grey10") - sethue(nodecolor) - drawgraph(g, layout=shell, - margin=50, - edgegaps=30, - edgestrokeweights=2, - vertexshapes=:circle, - vertexshapesizes=30, - vertexlabels=(v) -> "n$(node_id_map[v]) ($(length(topology.graph[node_id_map[v]].agents)))", - vertexfunction=(v, c) -> begin - @layer begin - sethue(nodecolor) - circle(c[v], 25, :fill) - sethue(textcolor) - t_p = c[v] + (0, 10) - translate(t_p) - label("n$(node_id_map[v]) ($(length(topology.graph[node_id_map[v]].agents)))") - if annotate_aids - translate(Karnak.Point(50, 0)) - label(string([aid(a) for a in topology.graph[node_id_map[v]].agents])) - end - end - end, - edgestrokecolors=edgecolor, - edgelabelcolors=edgecolor, - edgelabels=(n, s, d, f, t) -> begin - θ = slope(f, t) - fontsize(12) - translate(midpoint(f, t)) - rotate(θ) - sethue(textcolor) - label("state $(g[node_id_map[s],node_id_map[d]])", offset=-15) - end) - end dimensions[1] dimensions[2] - - svg = svgstring() - if !isnothing(write_to) - write(write_to, svg) - end - return svg -end - function _to_seconds(date::DateTime, init::DateTime) return (date - init).value / 1000 end @@ -84,8 +26,6 @@ function show_communication_data(topology::Topology, resolution_s::Real=0.1, display::Bool=true) - GLMakie.activate!() - fig = Figure() ax = Axis(fig[1, 1]) diff --git a/src/visualization/observation.jl b/src/visualization/observation.jl index a1f8d52b..01bfbfdb 100644 --- a/src/visualization/observation.jl +++ b/src/visualization/observation.jl @@ -1,10 +1,9 @@ export plot_world, plot_agents, plot_recordings using CairoMakie -using GLMakie +CairoMakie.activate!() function plot_world(world::World, recording::String; write_to::Union{Nothing,String}="world_observation.svg", fig=Figure()) - CairoMakie.activate!() data = data_collection(world, recording) ax = Axis(fig, @@ -19,7 +18,6 @@ function plot_world(world::World, recording::String; write_to::Union{Nothing,Str end function plot_agents(world::World, recording::String; write_to::Union{Nothing,String}="agent_observation.svg", fig=Figure()) - CairoMakie.activate!() data = data_agent_collection(world, recording) ax = Axis(fig, @@ -46,7 +44,6 @@ function _create_label(layout, label) end function plot_recordings(world::World; write_to::Union{Nothing,String}="observation.svg", size=(600, 600)) - CairoMakie.activate!() dc = world.data_collections dac = world.data_agent_collections diff --git a/test/visualization_tests.jl b/test/visualization_tests.jl index 0d607a08..201b4b1c 100644 --- a/test/visualization_tests.jl +++ b/test/visualization_tests.jl @@ -6,24 +6,6 @@ using Dates @agent struct TopologyPlotAgent end -@testset "TestTopologyPlotting" begin - topology = create_topology() do topology - n1 = add_node!(topology, TopologyPlotAgent()) - n2 = add_node!(topology, TopologyPlotAgent()) - n3 = add_node!(topology, TopologyPlotAgent(), id=12) - add_edge!(topology, n1, n2) - add_edge!(topology, n1, n3) - end - - svg_string = plot(topology, write_to="test_topology_plot.svg") - svg_string2 = plot(topology, annotate_aids=true) - - @test length(svg_string) > 10000 - @test length(svg_string2) > 20000 - - rm("test_topology_plot.svg") -end - @agent struct MyVisuBehavingAgent counter::Int other_aid::String From fd4f4213f8acb3eb5aa008bc03652d5afbc25c71 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Mon, 27 Jan 2025 14:49:41 +0100 Subject: [PATCH 30/54] Removed CairoMakie, Added simple topology plotting method. --- Project.toml | 6 +++-- src/util/topology.jl | 6 +++-- src/visualization/communication.jl | 35 +++++++++++++++++++++++++----- src/visualization/observation.jl | 5 ++--- test/visualization_tests.jl | 17 ++++++++++++++- 5 files changed, 56 insertions(+), 13 deletions(-) diff --git a/Project.toml b/Project.toml index d82ae15b..acab4f1d 100644 --- a/Project.toml +++ b/Project.toml @@ -5,7 +5,6 @@ repo = "https://github.com/OFFIS-DAI/Mango.jl" version = "0.4.0" [deps] -CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" ConcurrentCollections = "5060bff5-0b44-40c5-b522-fcd3ca5cecdd" ConcurrentUtilities = "f0e56b4a-5159-44fe-b623-3e5288b988bb" @@ -16,6 +15,7 @@ Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LightBSON = "a4a7f996-b3a6-4de6-b9db-2fa5f350df41" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" Mosquitto = "db317de6-444b-4dfa-9d0e-fbf3d8dd78ea" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" @@ -32,6 +32,7 @@ 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" @@ -39,8 +40,9 @@ Parameters = "~0.12" julia = "^1.9" [extras] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "Documenter"] +test = ["Test", "Documenter", "CairoMakie"] diff --git a/src/util/topology.jl b/src/util/topology.jl index c2b2ab0c..65c09157 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -229,9 +229,11 @@ Assign all agents of the `container` to the nodes of the `topology`. The agents to the nodes in the order of the nodes in the graph. """ function auto_assign!(topology::Topology, container::ContainerInterface) - for (i, label) in enumerate(labels(topology.graph)) + index_to_label = collect(labels(topology.graph)) + for (i, agent) in enumerate(agents(container)) + label = index_to_label[(((i-1)%length(index_to_label))+1)] node = topology.graph[label] - add!(node, container[i]) + add!(node, agent) end _build_neighborhoods_and_inject(topology) end diff --git a/src/visualization/communication.jl b/src/visualization/communication.jl index 5e77f7fb..07b77871 100644 --- a/src/visualization/communication.jl +++ b/src/visualization/communication.jl @@ -1,10 +1,35 @@ -export plot, show_communication_data +export plot_topology, show_communication_data +using Makie using GraphMakie.NetworkLayout using GraphMakie using Graphs using Dates +function plot_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 _to_seconds(date::DateTime, init::DateTime) return (date - init).value / 1000 end @@ -24,7 +49,7 @@ function show_communication_data(topology::Topology, messages::Vector{MessageTransaction}, initial_time::DateTime=DateTime(0); resolution_s::Real=0.1, - display::Bool=true) + show::Bool=true) fig = Figure() ax = Axis(fig[1, 1]) @@ -123,7 +148,7 @@ function show_communication_data(topology::Topology, hidedecorations!(ax) hidespines!(ax) - if display + if show wait(display(fig)) end return fig @@ -132,7 +157,7 @@ end function show_communication_data(topology::Topology, world::World; resolution_s::Real=0.1, - display::Bool=true) + show::Bool=true) return show_communication_data(topology, world.recorded_messages, world.initial_time, - resolution_s=resolution_s, display=display) + resolution_s=resolution_s, show=show) end \ No newline at end of file diff --git a/src/visualization/observation.jl b/src/visualization/observation.jl index 01bfbfdb..76006587 100644 --- a/src/visualization/observation.jl +++ b/src/visualization/observation.jl @@ -1,7 +1,6 @@ export plot_world, plot_agents, plot_recordings -using CairoMakie -CairoMakie.activate!() +using Makie function plot_world(world::World, recording::String; write_to::Union{Nothing,String}="world_observation.svg", fig=Figure()) @@ -28,7 +27,7 @@ function plot_agents(world::World, recording::String; write_to::Union{Nothing,St pairs = collect(data.timeseries) labels = [pair[1] for pair in pairs] values = [pair[2] for pair in pairs] - series!(ax, data.time, hcat(values...)', labels=labels, color=:coolwarm) + series!(ax, data.time, hcat(values...)', labels=labels) axislegend(ax, position=:lt) if !isnothing(write_to) save(write_to, fig) diff --git a/test/visualization_tests.jl b/test/visualization_tests.jl index 201b4b1c..48703373 100644 --- a/test/visualization_tests.jl +++ b/test/visualization_tests.jl @@ -2,6 +2,7 @@ using Mango using Test using Graphs using Dates +using CairoMakie @agent struct TopologyPlotAgent end @@ -67,5 +68,19 @@ end results = discrete_step_until(world, 1000) end - show_communication_data(topology, world, display=false) + show_communication_data(topology, world, show=false) +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_topology(topology, write_to="test_topology_plot.svg") + rm("test_topology_plot.svg") end \ No newline at end of file From cea5f05c798a5a64aead7a8f75ca8e22228ccdc5 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 28 Jan 2025 17:23:23 +0100 Subject: [PATCH 31/54] Adding color. Interactive elements to the communication plot. --- src/visualization/communication.jl | 81 +++++++++++++++++++++++++----- src/visualization/observation.jl | 25 ++++++--- 2 files changed, 86 insertions(+), 20 deletions(-) diff --git a/src/visualization/communication.jl b/src/visualization/communication.jl index 07b77871..b55f7e40 100644 --- a/src/visualization/communication.jl +++ b/src/visualization/communication.jl @@ -37,9 +37,7 @@ 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, :sender - elseif collect(labels(graph))[dst(edge)] == sender_id && collect(labels(graph))[src(edge)] == receiver_id - return i, edge, :receiver + return i, edge end end return -1 @@ -49,9 +47,20 @@ function show_communication_data(topology::Topology, messages::Vector{MessageTransaction}, initial_time::DateTime=DateTime(0); resolution_s::Real=0.1, - show::Bool=true) + show::Bool=true, + size=(1200, 800)) + + g = topology.graph + + if !is_directed(topology.graph) + edge_data = [[(e[1], e[2]) => topology.graph[e[1], e[2]] for e in edge_labels(topology.graph)]; + [(e[2], e[1]) => topology.graph[e[1], e[2]] for e in edge_labels(topology.graph)]] + vertex_data = [l => topology.graph[l] for l in labels(topology.graph)] + underlying_graph = DiGraph(topology.graph.graph) + g = MetaGraph(underlying_graph, vertex_data, edge_data) + end - fig = Figure() + fig = Figure(size=size) ax = Axis(fig[1, 1]) min_date = initial_time @@ -64,7 +73,6 @@ function show_communication_data(topology::Topology, sliderobservable = sg.sliders[1].value - g = topology.graph aid_to_node_id = Dict{String,Int}() for label in labels(topology.graph) node = topology.graph[label] @@ -87,20 +95,34 @@ function show_communication_data(topology::Topology, end edgecolors end - elabels = lift(sliderobservable) do time - elabels = ["" for _ in 1:ne(g)] + 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 - elabels[found[1]] = "$(typeof(message.content)): $(last("$(message.content)", 5))" + i_efull[found[1]] = "$(message.content)" end end end - elabels + i_efull end arrow_markers = lift(sliderobservable) do time @@ -111,7 +133,7 @@ function show_communication_data(topology::Topology, found = _find_edge(g, aid_to_node_id[message.sender_id], aid_to_node_id[message.receiver_id]) if found != -1 - markers[found[1]] = found[3] == :sender ? :rtriangle : :ltriangle + markers[found[1]] = :rtriangle end end end @@ -133,23 +155,56 @@ function show_communication_data(topology::Topology, shifts end - graphplot!(ax, g, layout=Shell(), + 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 + + p = graphplot!(ax, g, layout=Shell(), edge_color=edgecolors, elabels=elabels, arrow_show=true, + edge_width=edge_width, node_size=48, node_color=:gray, + node_strokewidth=0, arrow_size=24, arrow_shift=arrow_shifts, arrow_marker=arrow_markers, ilabels=repr.(1:nv(g)), - ilabels_color=:white) + 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 wait(display(fig)) + else + save("communication.svg", fig) end return fig end diff --git a/src/visualization/observation.jl b/src/visualization/observation.jl index 76006587..04ccb31b 100644 --- a/src/visualization/observation.jl +++ b/src/visualization/observation.jl @@ -2,7 +2,11 @@ export plot_world, plot_agents, plot_recordings using Makie -function plot_world(world::World, recording::String; write_to::Union{Nothing,String}="world_observation.svg", fig=Figure()) +function 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, @@ -10,13 +14,16 @@ function plot_world(world::World, recording::String; write_to::Union{Nothing,Str xlabel="time (seconds)", ylabel=recording, ) - lines!(ax, data.time, data.timeseries) + lines!(ax, data.time, data.timeseries, color=color, colormap=colormap) if !isnothing(write_to) save(write_to, fig) end end -function plot_agents(world::World, recording::String; write_to::Union{Nothing,String}="agent_observation.svg", fig=Figure()) +function plot_agents(world::World, recording::String; + write_to::Union{Nothing,String}="agent_observation.png", + fig=Figure(), + color=:viridis) data = data_agent_collection(world, recording) ax = Axis(fig, @@ -27,7 +34,7 @@ function plot_agents(world::World, recording::String; write_to::Union{Nothing,St pairs = collect(data.timeseries) labels = [pair[1] for pair in pairs] values = [pair[2] for pair in pairs] - series!(ax, data.time, hcat(values...)', labels=labels) + series!(ax, data.time, hcat(values...)', labels=labels, color=color) axislegend(ax, position=:lt) if !isnothing(write_to) save(write_to, fig) @@ -42,7 +49,11 @@ function _create_label(layout, label) halign=:left) end -function plot_recordings(world::World; write_to::Union{Nothing,String}="observation.svg", size=(600, 600)) +function plot_recordings(world::World; + write_to::Union{Nothing,String}="observation.png", + size=(600, 600), + color=:black, + colormap=:viridis) dc = world.data_collections dac = world.data_agent_collections @@ -51,12 +62,12 @@ function plot_recordings(world::World; write_to::Union{Nothing,String}="observat agent_layout = main_fig[2, 1] = GridLayout() for (i, key) in enumerate(keys(dc)) - plot_world(world, key, write_to=nothing, fig=world_layout[1, i]) + plot_world(world, key, write_to=nothing, fig=world_layout[1, i], color=color, colormap=colormap) end _create_label(world_layout, "W") for (i, key) in enumerate(keys(dac)) - plot_agents(world, key, write_to=nothing, fig=agent_layout[1, i]) + plot_agents(world, key, write_to=nothing, fig=agent_layout[1, i], color=colormap) end _create_label(agent_layout, "A") From 3fa50d1fdbea5b9e78ae0d9f9e560ed2af47b158 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Wed, 29 Jan 2025 14:14:53 +0100 Subject: [PATCH 32/54] Adding bridge from agent to environment and more flexible topologies. --- src/Mango.jl | 1 + src/agent/core.jl | 1 + src/environment/api.jl | 33 +++++++++++++ src/environment/core.jl | 37 ++------------ src/util/topology.jl | 79 +++++++++++++++++------------- src/visualization/communication.jl | 2 +- src/visualization/observation.jl | 2 +- 7 files changed, 84 insertions(+), 71 deletions(-) create mode 100644 src/environment/api.jl diff --git a/src/Mango.jl b/src/Mango.jl index 77914e3d..78887287 100644 --- a/src/Mango.jl +++ b/src/Mango.jl @@ -9,6 +9,7 @@ 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") diff --git a/src/agent/core.jl b/src/agent/core.jl index 432f5bcf..a2de28a5 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -33,6 +33,7 @@ for the agent. """ struct AgentContext container::ContainerInterface + evironment::EnvironmentInterface end """ diff --git a/src/environment/api.jl b/src/environment/api.jl new file mode 100644 index 00000000..9c641d1e --- /dev/null +++ b/src/environment/api.jl @@ -0,0 +1,33 @@ +export Position, Space, WorldObserver, Behavior + +abstract type Position end +abstract type Space{P<:Position} end +abstract type WorldObserver end +abstract type Behavior end +abstract type EnvironmentInterface end + +function dispatch_global_event(observer::WorldObserver, event::Any) + # default no reaction +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 + +function initialize(space::Space, agents::Vector{A}) where {A<:Agent} + throw("Initialization for $space is not defined!") +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 \ No newline at end of file diff --git a/src/environment/core.jl b/src/environment/core.jl index 0fe14c88..c366b881 100644 --- a/src/environment/core.jl +++ b/src/environment/core.jl @@ -1,16 +1,7 @@ -export Environment, Space, Position, Position2D, Area2D, location, - move, initialize, initialized, Behavior, schedule, WorldObserver, +export Environment, Position2D, Area2D, location, + move, initialize, initialized, schedule, emit_global_event, behavior -abstract type Position end -abstract type Space{P<:Position} end -abstract type WorldObserver end -abstract type Behavior end - -function dispatch_global_event(observer::WorldObserver, event::Any) - # default no reaction -end - struct NoBehavior <: Behavior end struct Position2D <: Position @@ -31,7 +22,7 @@ The environment is a separate entity, which describes some type of environment, 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 Environment{S<:Space} +@kwdef mutable struct Environment{S<:Space} <: EnvironmentInterface scheduler::SimulationScheduler space::S = Area2D(width=10, height=10) behavior::Behavior = NoBehavior() @@ -41,7 +32,6 @@ end schedule(f::Function, environment::Environment, data::TaskData) = schedule(f, environment.scheduler, data) - """ on_step(behavior::Behavior, environment::Environment, clock::Clock, step_size_s::Real) @@ -55,36 +45,15 @@ function step(env::Environment, clock::Clock, step_size_s::Real) on_step(behavior(env), env, clock, step_size_s) 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 - function location(space::Area2D, agent::Agent)::Position2D return space.to_position[aid(agent)] 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 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) diff --git a/src/util/topology.jl b/src/util/topology.jl index 65c09157..0834c6c5 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -23,11 +23,14 @@ end end @kwdef mutable struct TopologyService - state_to_neighbors::Dict{State,Vector{AgentAddress}} = Dict() + tid_to_state_to_neighbors::Dict{Symbol,Dict{State,Vector{AgentAddress}}} = Dict() end -function neighbors(service::TopologyService, state::State=NORMAL) - return get(service.state_to_neighbors, state, Vector()) +function neighbors(service::TopologyService, tid::Symbol=:default, state::State=NORMAL) + if haskey(service.tid_to_state_to_neighbors, tid) + return get(service.tid_to_state_to_neighbors[tid], state, Vector()) + end + throw(ArgumentError("No neighbors found for tid=$tid")) end function _create_meta_graph_with(graph::AbstractGraph) @@ -129,8 +132,26 @@ function set_edge_state!(topology::Topology, node_id_from::Int, node_id_to::Int, topology.graph[node_id_from, node_id_to] = state end +function _build_neighborhoods_and_inject(topology::Topology, tid::Symbol=:default) + # 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.tid_to_state_to_neighbors[tid] = state_to_neighbors + end + end +end + """ - create_topology(create_runnable)::Topology + 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. @@ -149,10 +170,10 @@ topology = create_topology() do topology end ``` """ -function create_topology(create_runnable::Function; directed::Bool=false) +function create_topology(create_runnable::Function; tid::Symbol=:default, directed::Bool=false) topology = Topology(_create_meta_graph_with(directed ? DiGraph() : Graph())) create_runnable(topology) - _build_neighborhoods_and_inject(topology) + _build_neighborhoods_and_inject(topology, tid) return topology end @@ -176,30 +197,12 @@ modify_topology(my_topology) do topology end ``` """ -function modify_topology(modify_runnable::Function, topology::Topology) +function modify_topology(modify_runnable::Function, topology::Topology; tid::Symbol=:default) modify_runnable(topology) - _build_neighborhoods_and_inject(topology) + _build_neighborhoods_and_inject(topology, tid) 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) @@ -213,13 +216,13 @@ per_node(topology) do node end ``` """ -function per_node(assign_runnable::Function, topology::Topology) +function per_node(assign_runnable::Function, topology::Topology; tid::Symbol=:default) # 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) + _build_neighborhoods_and_inject(topology, tid) end """ @@ -228,14 +231,14 @@ end 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) +function auto_assign!(topology::Topology, container::ContainerInterface; tid::Symbol=:default) index_to_label = collect(labels(topology.graph)) for (i, agent) in enumerate(agents(container)) label = index_to_label[(((i-1)%length(index_to_label))+1)] node = topology.graph[label] add!(node, agent) end - _build_neighborhoods_and_inject(topology) + _build_neighborhoods_and_inject(topology, tid) end """ @@ -280,17 +283,23 @@ function choose_agent(choose_agent_function::Function, topology::Topology) end """ - topology_neighbors(agent) + 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, state::State=NORMAL)::Vector{AgentAddress} - return neighbors(service_of_type(agent, TopologyService, TopologyService()), state) +function topology_neighbors(agent::Agent; tid::Symbol=:default, state::State=NORMAL)::Vector{AgentAddress} + return neighbors(service_of_type(agent, TopologyService, TopologyService()), tid, state) end -function topology_neighbors(role::Role, state::State=NORMAL)::Vector{AgentAddress} - return neighbors(service_of_type(role.context.agent, TopologyService, TopologyService()), state) +""" + topology_neighbors(role::Role; 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(role::Role; tid::Symbol=:default, state::State=NORMAL)::Vector{AgentAddress} + return neighbors(service_of_type(role.context.agent, TopologyService, TopologyService()), tid, state) end # Graphs API calls forwarded to Topology diff --git a/src/visualization/communication.jl b/src/visualization/communication.jl index b55f7e40..605697d7 100644 --- a/src/visualization/communication.jl +++ b/src/visualization/communication.jl @@ -202,7 +202,7 @@ function show_communication_data(topology::Topology, register_interaction!(ax, :ehover, ehover) if show - wait(display(fig)) + return display(fig) else save("communication.svg", fig) end diff --git a/src/visualization/observation.jl b/src/visualization/observation.jl index 04ccb31b..17c8727b 100644 --- a/src/visualization/observation.jl +++ b/src/visualization/observation.jl @@ -72,6 +72,6 @@ function plot_recordings(world::World; _create_label(agent_layout, "A") if !isnothing(write_to) - save(write_to, main_fig) + return save(write_to, main_fig) end end \ No newline at end of file From f112483d17cf69c2e4b0f963a4e2cc385c98360d Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Thu, 30 Jan 2025 22:13:04 +0100 Subject: [PATCH 33/54] Adding environment interface, adding to AgentContext. --- src/Mango.jl | 2 +- src/agent/core.jl | 2 +- src/container/core.jl | 3 ++- src/environment/api.jl | 44 +++++++++++++++++++++++++++++++++-- src/environment/core.jl | 32 ++++++++++++------------- src/simulation/container.jl | 3 ++- src/simulation/world.jl | 4 ++-- test/environment_api_tests.jl | 2 +- test/visualization_tests.jl | 2 +- 9 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/Mango.jl b/src/Mango.jl index 78887287..38aa3857 100644 --- a/src/Mango.jl +++ b/src/Mango.jl @@ -14,6 +14,7 @@ include("environment/api.jl") include("agent/role.jl") include("agent/core.jl") +include("environment/core.jl") include("container/protocol.jl") include("container/tcp.jl") include("container/mqtt.jl") @@ -23,7 +24,6 @@ include("simulation/tasks.jl") include("container/core.jl") include("simulation/container.jl") -include("environment/core.jl") include("simulation/world.jl") include("util/topology.jl") include("visualization/communication.jl") diff --git a/src/agent/core.jl b/src/agent/core.jl index a2de28a5..dfdde58c 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -33,7 +33,7 @@ for the agent. """ struct AgentContext container::ContainerInterface - evironment::EnvironmentInterface + evironment::Environment end """ diff --git a/src/container/core.jl b/src/container/core.jl index 8983b391..8d308f4b 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)" @@ -104,7 +105,7 @@ function register( end container.agents[actual_aid] = agent agent.aid = actual_aid - agent.context = AgentContext(container) + agent.context = AgentContext(container, env) container.agent_counter += 1 if !isnothing(container.protocol) diff --git a/src/environment/api.jl b/src/environment/api.jl index 9c641d1e..3012a56e 100644 --- a/src/environment/api.jl +++ b/src/environment/api.jl @@ -1,10 +1,12 @@ -export Position, Space, WorldObserver, Behavior +export Position, Space, WorldObserver, Behavior, Environment abstract type Position end abstract type Space{P<:Position} end abstract type WorldObserver end abstract type Behavior end -abstract type EnvironmentInterface end +abstract type Environment end + +struct NoEnv <: Environment end function dispatch_global_event(observer::WorldObserver, event::Any) # default no reaction @@ -30,4 +32,42 @@ 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 + +function schedule(f::Function, environment::Environment, data::TaskData) + # default do nothing +end + +function step(environment::Environment, clock::Clock, step_size_s::Real) + # default do nothing +end + +function initialize(environment::Environment, agents::Vector{A}) where {A<:Agent} + # default do nothing +end + +function initialized(environment::Environment) + # default do nothing +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 \ No newline at end of file diff --git a/src/environment/core.jl b/src/environment/core.jl index c366b881..4e5fce19 100644 --- a/src/environment/core.jl +++ b/src/environment/core.jl @@ -1,4 +1,4 @@ -export Environment, Position2D, Area2D, location, +export DefaultEnvironment, Position2D, Area2D, location, move, initialize, initialized, schedule, emit_global_event, behavior @@ -16,13 +16,13 @@ end end """ -Struct Environment. The environment is meant to provide a description of everything which exists outside of the agents. +Struct DefaultEnvironment. The environment is meant to provide 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 Environment{S<:Space} <: EnvironmentInterface +@kwdef mutable struct DefaultEnvironment{S<:Space} <: Environment scheduler::SimulationScheduler space::S = Area2D(width=10, height=10) behavior::Behavior = NoBehavior() @@ -30,18 +30,18 @@ with the environment and exist in the defined space. initialized::Bool = false end -schedule(f::Function, environment::Environment, data::TaskData) = schedule(f, environment.scheduler, data) +schedule(f::Function, environment::DefaultEnvironment, data::TaskData) = schedule(f, environment.scheduler, data) """ - on_step(behavior::Behavior, environment::Environment, clock::Clock, step_size_s::Real) + on_step(behavior::Behavior, environment::DefaultEnvironment, clock::Clock, step_size_s::Real) Called on stepping the container. """ -function on_step(behavior::Behavior, environment::Environment, clock::Clock, step_size_s::Real) +function on_step(behavior::Behavior, environment::DefaultEnvironment, clock::Clock, step_size_s::Real) # default do nothing end -function step(env::Environment, clock::Clock, step_size_s::Real) +function step(env::DefaultEnvironment, clock::Clock, step_size_s::Real) on_step(behavior(env), env, clock, step_size_s) end @@ -64,49 +64,49 @@ function initialize(behavior::Behavior) # default no initialization end -function initialize(environment::Environment{S}, agents::Vector{A}) where {S<:Space} where {A<:Agent} +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::Environment) + initialized(environment::DefaultEnvironment) Return whether the environment is intialized. """ -function initialized(environment::Environment) +function initialized(environment::DefaultEnvironment) return environment.initialized end """ - add_observer!(environment::Environment, observer::WorldObserver) + 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::Environment, observer::WorldObserver) +function add_observer!(environment::DefaultEnvironment, observer::WorldObserver) push!(environment.observers, observer) end """ - behavior(env::Environment) + behavior(env::DefaultEnvironment) Return the behavior of the environment. """ -function behavior(env::Environment) +function behavior(env::DefaultEnvironment) return env.behavior end """ - emit_global_event(environment::Environment, event::Any) + emit_global_event(environment::DefaultEnvironment, 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) +function emit_global_event(environment::DefaultEnvironment, event::Any) for observer in environment.observers dispatch_global_event(observer, event) end diff --git a/src/simulation/container.jl b/src/simulation/container.jl index e57f13e8..11b1e034 100644 --- a/src/simulation/container.jl +++ b/src/simulation/container.jl @@ -11,6 +11,7 @@ end @kwdef mutable struct SimulationContainer <: ContainerInterface clock::Clock + env::Environment current_step_size_s::Real = 0 agents::OrderedDict{String,Agent} = OrderedDict{String,Agent}() agent_counter::Integer = 0 @@ -38,7 +39,7 @@ function register( end container.agents[actual_aid] = agent agent.aid = actual_aid - agent.context = AgentContext(container) + agent.context = AgentContext(container, container.env) container.agent_counter += 1 return agent diff --git a/src/simulation/world.jl b/src/simulation/world.jl index c7b2cbc5..998b1f77 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -98,8 +98,8 @@ The World used as a base struct to enable simulations in Mango.jl. Always create @kwdef mutable struct World <: ContainerInterface clock::Clock = Clock(DateTime(0)) initial_time::DateTime = DateTime(0) - container::SimulationContainer = SimulationContainer(clock=clock) - env::Environment = Environment(scheduler=SimulationScheduler(clock=clock)) + 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) diff --git a/test/environment_api_tests.jl b/test/environment_api_tests.jl index 35ef1576..33a8b63d 100644 --- a/test/environment_api_tests.jl +++ b/test/environment_api_tests.jl @@ -17,7 +17,7 @@ end struct TestBehavior <: Behavior end -function Mango.on_step(behavior::TestBehavior, environment::Environment, clock::Clock, step_size_s::Real) +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") diff --git a/test/visualization_tests.jl b/test/visualization_tests.jl index 48703373..73c208f7 100644 --- a/test/visualization_tests.jl +++ b/test/visualization_tests.jl @@ -12,7 +12,7 @@ end other_aid::String end -function Mango.on_step(agent::MyVisuBehavingAgent, environment::Environment, clock::Clock, step_size_s::Real) +function Mango.on_step(agent::MyVisuBehavingAgent, environment::DefaultEnvironment, clock::Clock, step_size_s::Real) if agent.counter > 10 return end From 4404a13f4660fa8417f026dcd07cebcb2174ad14 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Fri, 31 Jan 2025 16:27:21 +0100 Subject: [PATCH 34/54] Reviews comments, docs, tests for exception handling. --- docs/src/simulation.md | 5 ++++- src/environment/core.jl | 6 +++--- src/simulation/container.jl | 8 ++++---- src/simulation/tasks.jl | 4 ++-- src/simulation/world.jl | 8 ++++---- src/util/scheduling.jl | 2 +- test/visualization_tests.jl | 1 + test/world_tests.jl | 33 +++++++++++++++++++++++++++++++++ 8 files changed, 52 insertions(+), 15 deletions(-) diff --git a/docs/src/simulation.md b/docs/src/simulation.md index 83932c38..fa00b27a 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_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. +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. diff --git a/src/environment/core.jl b/src/environment/core.jl index 0fe14c88..1829f2a6 100644 --- a/src/environment/core.jl +++ b/src/environment/core.jl @@ -25,7 +25,7 @@ end end """ -Struct Environment. The environment is meant to provide a description of everything which exists outside of the agents. +Struct Environment. 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 @@ -132,9 +132,9 @@ end """ emit_global_event(environment::Environment, event::Any) -Emit an global event. This types of events can be handled by any agent +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 event emitted with +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::Environment, event::Any) diff --git a/src/simulation/container.jl b/src/simulation/container.jl index e57f13e8..3c783eea 100644 --- a/src/simulation/container.jl +++ b/src/simulation/container.jl @@ -1,17 +1,17 @@ """ -Represents a message data package including the arriving time of the package. +Represents a message data package including the arrival time of the package. """ struct MessageData content::Any meta::AbstractDict - arriving_time::DateTime + arrival_time::DateTime end @kwdef mutable struct SimulationContainer <: ContainerInterface clock::Clock - current_step_size_s::Real = 0 + step_size_s::Real = 0 agents::OrderedDict{String,Agent} = OrderedDict{String,Agent}() agent_counter::Integer = 0 shutdown::Bool = false @@ -48,7 +48,7 @@ function forward_message(container::SimulationContainer, msg::Any, meta::Abstrac push!(container.message_queue, MessageData(msg, meta, - add_seconds(time(container), container.current_step_size_s) + add_seconds(time(container), container.step_size_s) ) ) return NonWaitable() diff --git a/src/simulation/tasks.jl b/src/simulation/tasks.jl index 0d24d961..468e3193 100644 --- a/src/simulation/tasks.jl +++ b/src/simulation/tasks.jl @@ -105,11 +105,11 @@ function execute_task_for(task_sim::SimpleTaskSimulation, break end - # Every time a task is running the state can change, so another iteration has to be calced + # 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 isa(task, Task) + if task isa Task @debug "Continue the old Task!" task notify(scheduler.events[task][1]) else diff --git a/src/simulation/world.jl b/src/simulation/world.jl index c7b2cbc5..3fc4e706 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -76,7 +76,7 @@ A WorldRecording is a container to record data in the world. end """ -A AgentsRecording is a container to record data of the agents. +An AgentsRecording is a container to record data of the agents. """ @kwdef mutable struct AgentsRecording timeseries::Dict{String,Vector{Any}} = Dict() @@ -179,7 +179,7 @@ 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)) + return MessagePackage(sender_aid, receiver_aid, message_data.arrival_time, (message_data.content, message_data.meta)) end """ @@ -352,7 +352,7 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ return nothing end end - world.container.current_step_size_s = time_step_s + world.container.step_size_s = time_step_s step_all_entities(world, time_step_s) @@ -391,7 +391,7 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ @debug "The simulation step needed $elapsed seconds" world.clock.simulation_time = add_seconds(time(world), time_step_s) - world.container.current_step_size_s = 0 + world.container.step_size_s = 0 @debug "New time" time(world) diff --git a/src/util/scheduling.jl b/src/util/scheduling.jl index feebcca5..4a14f40d 100644 --- a/src/util/scheduling.jl +++ b/src/util/scheduling.jl @@ -325,7 +325,7 @@ 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. +events fulfilling the purpose to step the tasks only for a given step_size. """ @kwdef struct SimulationScheduler <: AbstractScheduler clock::Clock diff --git a/test/visualization_tests.jl b/test/visualization_tests.jl index 48703373..307f68b7 100644 --- a/test/visualization_tests.jl +++ b/test/visualization_tests.jl @@ -69,6 +69,7 @@ end end show_communication_data(topology, world, show=false) + rm("communication.svg") end @testset "TestVisuAgentsTopo" begin diff --git a/test/world_tests.jl b/test/world_tests.jl index b3414c0b..58654ffc 100644 --- a/test/world_tests.jl +++ b/test/world_tests.jl @@ -391,4 +391,37 @@ end @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 \ No newline at end of file From b23306d2f67af6fc232f198c00315c851a60b7ff Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Sat, 15 Feb 2025 01:37:21 +0100 Subject: [PATCH 35/54] Adding observation action services. Several bug fixes. --- src/Mango.jl | 1 + src/agent/core.jl | 157 ++++++++++++++++++++++++----- src/agent/role.jl | 40 +++++++- src/agent/services.jl | 51 ++++++++++ src/environment/api.jl | 52 ++++++---- src/environment/core.jl | 67 +++++++++++- src/simulation/container.jl | 2 +- src/simulation/tasks.jl | 5 +- src/simulation/world.jl | 31 +++--- src/util/error_handling.jl | 17 +++- src/util/scheduling.jl | 34 ++++++- src/util/topology.jl | 28 +++-- src/visualization/communication.jl | 2 +- test/agent_tests.jl | 25 +++++ 14 files changed, 430 insertions(+), 82 deletions(-) create mode 100644 src/agent/services.jl diff --git a/src/Mango.jl b/src/Mango.jl index 38aa3857..d6858727 100644 --- a/src/Mango.jl +++ b/src/Mango.jl @@ -13,6 +13,7 @@ include("environment/api.jl") include("agent/role.jl") include("agent/core.jl") +include("agent/services.jl") include("environment/core.jl") include("container/protocol.jl") diff --git a/src/agent/core.jl b/src/agent/core.jl index dfdde58c..d25930b0 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -2,6 +2,7 @@ export @agent, AgentContext, AgentRoleHandler, handle_message, + handle_unanswered, add, schedule, stop_and_wait_for_all_tasks, @@ -17,7 +18,11 @@ export @agent, add_service!, services, on_global_event, - sender_address + sender_address, + send_and_handle_answers, + send_tracked_messages, + send_messages, + has_role using UUIDs @@ -65,6 +70,7 @@ AGENT_BASELINE_FIELDS::Vector = [ :(aid::Union{Nothing,String} = nothing), :(transaction_handler::Dict{String,Tuple} = Dict{String,Tuple}()), :(forwarding_rules::Vector{ForwardingRule} = Vector{ForwardingRule}()), + :(outgoing::Vector{Tuple} = Vector{Tuple}()), :(services::Dict{DataType,Any} = Dict{DataType,Any}()) ] @@ -128,6 +134,35 @@ 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(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 + """ 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 @@ -150,15 +185,14 @@ 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) + handle_transaction_message(agent, message, meta) else for role in agent.role_handler.roles handle_message(role, message, meta) @@ -170,6 +204,13 @@ function dispatch_message(agent::Agent, message::Any, meta::AbstractDict) end handle_message(agent, message, meta) 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 @@ -179,7 +220,7 @@ end 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]) + return AgentAddress(aid=meta[SENDER_ID], address=meta[SENDER_ADDR], tracking_id=haskey(meta, TRACKING_ID) ? meta[TRACKING_ID] : nothing) end """ @@ -193,6 +234,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) @@ -255,6 +307,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) @@ -347,6 +408,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) @@ -391,22 +461,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, + agent.aid; + 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( @@ -426,26 +513,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( @@ -455,8 +565,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, diff --git a/src/agent/role.jl b/src/agent/role.jl index f31b0cc6..00d9fb51 100644 --- a/src/agent/role.jl +++ b/src/agent/role.jl @@ -155,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) @@ -275,6 +279,10 @@ 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 @@ -300,6 +308,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, @@ -311,6 +327,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, @@ -321,10 +347,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 @@ -344,4 +380,4 @@ Handle global event. See [`emit_global_event`](@ref). """ function on_global_event(role::Role, event::Any) # to be overridden -end \ No newline at end of file +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/environment/api.jl b/src/environment/api.jl index 3012a56e..01d5955f 100644 --- a/src/environment/api.jl +++ b/src/environment/api.jl @@ -1,7 +1,6 @@ -export Position, Space, WorldObserver, Behavior, Environment +export Position, Space, WorldObserver, Behavior, Environment, install, dispatch_global_event, initialize, initialized, add_observer! abstract type Position end -abstract type Space{P<:Position} end abstract type WorldObserver end abstract type Behavior end abstract type Environment end @@ -12,42 +11,41 @@ function dispatch_global_event(observer::WorldObserver, event::Any) # default no reaction 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 + schedule(f::Function, environment::Environment, data::TaskData) -function initialize(space::Space, agents::Vector{A}) where {A<:Agent} - throw("Initialization for $space is not defined!") +Schedule a task for the given environment. +""" +function schedule(f::Function, environment::Environment, data::TaskData) + throw("Schedule is not implemented for $environment") end """ - location(space::Area2D, agent::Agent)::Position2D + step(environment::Environment, clock::Clock, step_size_s::Real) -Return the location of the `agent`. +Step the environment for the given time and advancing step_size_s. """ -function location(space::Space{P}, agent::Agent)::P where {P<:Position} - throw("Position on the space $space not defined!") -end - -function schedule(f::Function, environment::Environment, data::TaskData) - # default do nothing -end - function step(environment::Environment, clock::Clock, step_size_s::Real) - # default do nothing + 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) - # default do nothing + throw("Initialized is not implemented for $environment") end """ @@ -70,4 +68,14 @@ 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 index d7b5f283..86bda07c 100644 --- a/src/environment/core.jl +++ b/src/environment/core.jl @@ -1,9 +1,48 @@ export DefaultEnvironment, Position2D, Area2D, location, move, initialize, initialized, schedule, - emit_global_event, behavior + 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 @@ -41,6 +80,15 @@ function on_step(behavior::Behavior, environment::DefaultEnvironment, clock::Clo # 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 @@ -49,7 +97,6 @@ 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 @@ -98,6 +145,15 @@ 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) @@ -110,4 +166,9 @@ function emit_global_event(environment::DefaultEnvironment, event::Any) for observer in environment.observers dispatch_global_event(observer, event) end -end \ No newline at end of file +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/simulation/container.jl b/src/simulation/container.jl index 62db04d6..9232d961 100644 --- a/src/simulation/container.jl +++ b/src/simulation/container.jl @@ -85,7 +85,7 @@ function process_message(container::SimulationContainer, msg::Any, meta::Abstrac 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 + @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) diff --git a/src/simulation/tasks.jl b/src/simulation/tasks.jl index 468e3193..481720d7 100644 --- a/src/simulation/tasks.jl +++ b/src/simulation/tasks.jl @@ -134,7 +134,7 @@ function execute_task_for(task_sim::SimpleTaskSimulation, # rethrow exception if exists if istaskfailed(task) - Base.show_backtrace(stderr, task.backtrace) + log_exception(task.exception, task.backtrace) throw(task.exception) end @@ -167,8 +167,7 @@ function step_iteration(task_sim::SimpleTaskSimulation, step_size_s::Real, first Threads.@spawn try execute_task_for(task_sim, scheduler, result, step_size_s) catch ex - bt = stacktrace(catch_backtrace()) - showerror(stderr, ex, bt) + log_exception(ex) rethrow(ex) end end diff --git a/src/simulation/world.jl b/src/simulation/world.jl index 38586969..fd8f9f46 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -1,7 +1,7 @@ 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_world!, record_agent!, record_agent_having! using Base.Threads using Dates @@ -37,8 +37,7 @@ function create_world(start_time::DateTime; behavior::Union{Nothing,Behavior}=nothing) world = World() - world.clock.simulation_time = start_time - world.initial_time = start_time + world.clock = Clock(start_time) if !isnothing(communication_sim) world.communication_sim = communication_sim end @@ -97,7 +96,6 @@ The World used as a base struct to enable simulations in Mango.jl. Always create """ @kwdef mutable struct World <: ContainerInterface clock::Clock = Clock(DateTime(0)) - initial_time::DateTime = DateTime(0) env::Environment = DefaultEnvironment(scheduler=SimulationScheduler(clock=clock)) container::SimulationContainer = SimulationContainer(clock=clock, env=env) task_sim::TaskSimulation = SimpleTaskSimulation(clock=clock) @@ -238,8 +236,7 @@ function cs_step_iteration(world::World, Threads.@spawn try process_message(world.container, mp.content[1], mp.content[2]) catch ex - bt = stacktrace(catch_backtrace()) - showerror(stderr, ex, bt) + log_exception(ex) rethrow(ex) end else @@ -290,7 +287,7 @@ end Record data `data` at time `time` in the `recording`. """ function insert_world_recording!(recording::WorldRecording, world::World, data::Any) - push!(recording.time, (time(world) - world.initial_time).value / 1000) + push!(recording.time, seconds_elapsed(clock(world))) push!(recording.timeseries, data) end @@ -368,16 +365,14 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ Threads.@spawn try comm_iter_result = cs_step_iteration(world, time_step_s, first_step ? comm_result : nothing) catch ex - bt = stacktrace(catch_backtrace()) - showerror(stderr, ex, bt) + log_exception(ex) rethrow(ex) end Threads.@spawn try task_iter_result = step_iteration(world.task_sim, time_step_s, first_step) catch ex - bt = stacktrace(catch_backtrace()) - showerror(stderr, ex, bt) + log_exception(ex) rethrow(ex) end end @@ -393,7 +388,7 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ world.clock.simulation_time = add_seconds(time(world), time_step_s) world.container.step_size_s = 0 - @debug "New time" time(world) + @info "New time" time(world) do_recordings(world) @@ -452,10 +447,10 @@ function register( suggested_aid::Union{String,Nothing}=nothing; kwargs..., ) - agent = register(world.container, agent, suggested_aid, 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 @@ -500,7 +495,7 @@ function collect_agent_data(collector::Function, world::World, key::String) for agent in values(agents(world)) push!(world.data_collectors, () -> collector(world, agent, dac)) end - push!(world.data_collectors, () -> push!(dac.time, (time(world) - world.initial_time).value / 1000)) + push!(world.data_collectors, () -> push!(dac.time, seconds_elapsed(clock(world)))) end """ @@ -529,6 +524,14 @@ function record_agent!(agent_recorder::Function, world::World, key::String) end end +function record_agent_having!(agent_recorder::Function, role_type::DataType, world::World, key::String) + collect_agent_data(world, key) do w, a, dc + if has_role(a, role_type) + insert_agent_recording!(dc, w, a, agent_recorder(a)) + end + end +end + function Base.getindex(world::World, index::String) return world.container[index] 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 4a14f40d..564e5e00 100644 --- a/src/util/scheduling.jl +++ b/src/util/scheduling.jl @@ -14,7 +14,8 @@ export TaskData, Scheduler, SimulationScheduler, AbstractScheduler, - sleep_until + sleep_until, + seconds_elapsed using Dates using ConcurrentCollections @@ -26,11 +27,22 @@ 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 """ @@ -39,6 +51,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 @@ -97,7 +116,7 @@ end Return the internal time representation, the `clock`. """ function clock(scheduler::AbstractScheduler)::AbstractClock - throw("unimplemented") + throw(InvalidStateException("unimplemented", :NotImplemented)) end """ tasks(scheduler::AbstractScheduler) @@ -105,7 +124,7 @@ end Return the tasks currently on schedule and managed by the scheduler. """ function tasks(scheduler::AbstractScheduler) - throw("unimplemented") + throw(InvalidStateException("unimplemented", :NotImplemented)) end """ @@ -217,6 +236,7 @@ function execute_task(f::Function, scheduler::AbstractScheduler, data::DelayTask end function execute_task(f::Function, scheduler::AbstractScheduler, data::DateTimeTaskData) + @info data.date now(scheduler) sleep(scheduler, (data.date - now(scheduler)).value / 1000) f() end @@ -343,6 +363,10 @@ struct WaitResult 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 @@ -422,6 +446,8 @@ 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)) @@ -429,7 +455,7 @@ function schedule(f::Function, scheduler::SimulationScheduler, data::TaskData) end function do_schedule(f::Function, scheduler::SimulationScheduler, data::TaskData, event::Base.Event) - task = @spawnlog execute_task(f, scheduler, data) + 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 index 0834c6c5..53ab52ac 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -1,7 +1,7 @@ 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!, - auto_assign! + auto_assign!, topology_node_id using MetaGraphsNext using Graphs @@ -24,6 +24,14 @@ end @kwdef mutable struct TopologyService tid_to_state_to_neighbors::Dict{Symbol,Dict{State,Vector{AgentAddress}}} = Dict() + tid_to_node_id::Dict{Symbol,Int} = 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 neighbors(service::TopologyService, tid::Symbol=:default, state::State=NORMAL) @@ -146,6 +154,7 @@ function _build_neighborhoods_and_inject(topology::Topology, tid::Symbol=:defaul for agent in node.agents topology_service = service_of_type(agent, TopologyService, TopologyService()) topology_service.tid_to_state_to_neighbors[tid] = state_to_neighbors + topology_service.tid_to_node_id[tid] = node.id end end end @@ -292,14 +301,21 @@ function topology_neighbors(agent::Agent; tid::Symbol=:default, state::State=NOR return neighbors(service_of_type(agent, TopologyService, TopologyService()), tid, state) end +function topology_neighbors(role::Role; tid::Symbol=:default, state::State=NORMAL)::Vector{AgentAddress} + return neighbors(service_of_type(role.context.agent, TopologyService, TopologyService()), tid, state) +end + """ - topology_neighbors(role::Role; tid::Symbol=:default, state::State=NORMAL)::Vector{AgentAddress} + topology_node_id(agent::Agent; tid::Symbol=:default)::Int -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`. +Retrieve the node id the `agent` is assigned to. """ -function topology_neighbors(role::Role; tid::Symbol=:default, state::State=NORMAL)::Vector{AgentAddress} - return neighbors(service_of_type(role.context.agent, TopologyService, TopologyService()), tid, state) +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 # Graphs API calls forwarded to Topology diff --git a/src/visualization/communication.jl b/src/visualization/communication.jl index 605697d7..123f6792 100644 --- a/src/visualization/communication.jl +++ b/src/visualization/communication.jl @@ -213,6 +213,6 @@ function show_communication_data(topology::Topology, world::World; resolution_s::Real=0.1, show::Bool=true) - return show_communication_data(topology, world.recorded_messages, world.initial_time, + return show_communication_data(topology, world.recorded_messages, world.clock.initial_time, resolution_s=resolution_s, show=show) end \ No newline at end of file diff --git a/test/agent_tests.jl b/test/agent_tests.jl index c4d498d1..ffd0d6d5 100644 --- a/test/agent_tests.jl +++ b/test/agent_tests.jl @@ -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) From 3317d791b11f451d6d8214e99ef6a1c260142456 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Sat, 15 Feb 2025 02:13:27 +0100 Subject: [PATCH 36/54] Fixing tests. --- src/agent/core.jl | 11 ++++++++++- src/simulation/world.jl | 3 +-- test/world_tests.jl | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/agent/core.jl b/src/agent/core.jl index d25930b0..2abcf17d 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -136,7 +136,7 @@ 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(meta) + sender = sender_address_tracked(meta) if length(addrs) == 1 if addrs[1] == sender push!(msgs, message) @@ -220,6 +220,15 @@ end 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 diff --git a/src/simulation/world.jl b/src/simulation/world.jl index fd8f9f46..58517592 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -36,8 +36,7 @@ function create_world(start_time::DateTime; space::Union{Nothing,Space}=nothing, behavior::Union{Nothing,Behavior}=nothing) - world = World() - world.clock = Clock(start_time) + world = World(clock=Clock(start_time)) if !isnothing(communication_sim) world.communication_sim = communication_sim end diff --git a/test/world_tests.jl b/test/world_tests.jl index 58654ffc..0c86b9f6 100644 --- a/test/world_tests.jl +++ b/test/world_tests.jl @@ -49,7 +49,7 @@ end send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid="abc")) - @test_logs (:warn, "Container $(keys(world.container.agents)) has no agent with id: abc") min_level = Logging.Warn begin + @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 From a210e17d0432560ce0dc0512a0231d385acdb368 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Wed, 26 Feb 2025 11:13:43 +0100 Subject: [PATCH 37/54] Fixed bug which occurred when a perdiodic task is scheduled with higher delay_s than communication delays. Limiting discrete_step_until to strictly smaller than time + max_advance_time_s. Some layouting changes to the observation visualization. --- Project.toml | 2 + src/agent/core.jl | 43 ++++++++- src/simulation/tasks.jl | 18 +++- src/simulation/world.jl | 29 ++++-- src/util/scheduling.jl | 23 ++--- src/util/topology.jl | 1 + src/visualization/communication.jl | 140 ++++++++++++++++++++++------- src/visualization/observation.jl | 18 ++-- test/visualization_tests.jl | 2 +- test/world_tests.jl | 60 +++++++++++++ 10 files changed, 272 insertions(+), 64 deletions(-) diff --git a/Project.toml b/Project.toml index acab4f1d..8b8eaa58 100644 --- a/Project.toml +++ b/Project.toml @@ -28,6 +28,7 @@ CairoMakie = "0.13.1" Colors = "~0.12" ConcurrentCollections = "~0.1" ConcurrentUtilities = "~2.2" +GLMakie = "0.11.2" GraphMakie = "0.5.13" Graphs = "~1.10" JSON = "~0.21" @@ -40,6 +41,7 @@ Parameters = "~0.12" julia = "^1.9" [extras] +GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/src/agent/core.jl b/src/agent/core.jl index 2abcf17d..1d158c66 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -22,7 +22,13 @@ export @agent, send_and_handle_answers, send_tracked_messages, send_messages, - has_role + has_role, + description, + name, + color, + category, + update_description, + AgentDescription using UUIDs @@ -58,6 +64,12 @@ struct ForwardingRule forward_replies::Bool end +mutable struct AgentDescription + name::String + category::Symbol + color::Symbol +end + """ All baseline fields added by the @agent macro are listed in this vector. They are added in the same order defined here. @@ -71,6 +83,7 @@ AGENT_BASELINE_FIELDS::Vector = [ :(transaction_handler::Dict{String,Tuple} = Dict{String,Tuple}()), :(forwarding_rules::Vector{ForwardingRule} = Vector{ForwardingRule}()), :(outgoing::Vector{Tuple} = Vector{Tuple}()), + :(description::AgentDescription = AgentDescription("", :agent, :gray)), :(services::Dict{DataType,Any} = Dict{DataType,Any}()) ] @@ -294,6 +307,34 @@ function aid(agent::Agent) return agent.aid end +function description(agent::Agent) + return agent.description +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 update_description(agent::Agent; color=nothing, name=nothing, category=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 + """ add(agent::Agent, role::Role) diff --git a/src/simulation/tasks.jl b/src/simulation/tasks.jl index 481720d7..d2329372 100644 --- a/src/simulation/tasks.jl +++ b/src/simulation/tasks.jl @@ -111,7 +111,20 @@ function execute_task_for(task_sim::SimpleTaskSimulation, task = something(next_task) if task isa Task @debug "Continue the old Task!" task - notify(scheduler.events[task][1]) + + # 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 @@ -128,9 +141,6 @@ function execute_task_for(task_sim::SimpleTaskSimulation, # clean up task data maybepop!(scheduler.tasks, task) - if haskey(scheduler.events, task) - maybepop!(scheduler.events, task) - end # rethrow exception if exists if istaskfailed(task) diff --git a/src/simulation/world.jl b/src/simulation/world.jl index 58517592..eb8daacf 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -312,14 +312,15 @@ function step_all_entities(world::World, time_step_s::Real) end """ - step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{SimulationResult,Nothing} + 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`. +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)::Union{SimulationResult,Nothing} +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))]) @@ -341,8 +342,8 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT)::Union{ 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) + @info "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 @@ -407,14 +408,15 @@ function discrete_step_until(world::World, max_advance_time_s::Real) 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) && - add_seconds(initial_time, max_advance_time_s) > time(world)) + max_time > time(world)) prev_time = time(world) - push!(results, step_simulation(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" @@ -523,9 +525,18 @@ function record_agent!(agent_recorder::Function, world::World, key::String) end end -function record_agent_having!(agent_recorder::Function, role_type::DataType, world::World, key::String) +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) + collect_agent_data(world, key) do w, a, dc - if has_role(a, role_type) + if has_role(a, role_type) && + (isnothing(color) || agent_color == color(a)) && + (isnothing(aid_contains) || occursin(aid_contains, aid(a))) + insert_agent_recording!(dc, w, a, agent_recorder(a)) end end diff --git a/src/util/scheduling.jl b/src/util/scheduling.jl index 564e5e00..786bd0c0 100644 --- a/src/util/scheduling.jl +++ b/src/util/scheduling.jl @@ -236,7 +236,6 @@ function execute_task(f::Function, scheduler::AbstractScheduler, data::DelayTask end function execute_task(f::Function, scheduler::AbstractScheduler, data::DateTimeTaskData) - @info data.date now(scheduler) sleep(scheduler, (data.date - now(scheduler)).value / 1000) f() end @@ -349,7 +348,8 @@ events fulfilling 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}}() + 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}() @@ -384,7 +384,7 @@ function determine_next_event_time_with(scheduler::SimulationScheduler, simulati # wait queue next = scheduler.wait_queue.head.next while !isnothing(next) - t = scheduler.events[next.value][2] + t = scheduler.task_time[next.value] if isnothing(lowest) || t < lowest lowest = t end @@ -399,13 +399,11 @@ 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) + 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) @@ -414,6 +412,8 @@ function wait_for_finish_or_sleeping(scheduler::SimulationScheduler, task::Task, else return WaitResult(false, Some(task.result)) end + sleep(check_delay_s) + remaining -= check_delay_s end throw("Simulation encountered a task timeout!") end @@ -425,10 +425,11 @@ 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] + if haskey(scheduler.task_time, current_task()) + ctime = scheduler.task_time[current_task()] end - scheduler.events[current_task()] = (event, add_seconds(ctime, time_s)) + 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 diff --git a/src/util/topology.jl b/src/util/topology.jl index 53ab52ac..bc70c45c 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -20,6 +20,7 @@ end 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 end @kwdef mutable struct TopologyService diff --git a/src/visualization/communication.jl b/src/visualization/communication.jl index 123f6792..d2d2339b 100644 --- a/src/visualization/communication.jl +++ b/src/visualization/communication.jl @@ -43,21 +43,91 @@ function _find_edge(graph::AbstractGraph, sender_id::Int, receiver_id::Int) return -1 end -function show_communication_data(topology::Topology, - messages::Vector{MessageTransaction}, +@agent struct VisuProxyAgent + proxy_aid::String +end + +function 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 show_communication_data(messages::Vector{MessageTransaction}, initial_time::DateTime=DateTime(0); resolution_s::Real=0.1, show::Bool=true, - size=(1200, 800)) + 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 - g = topology.graph + 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 - if !is_directed(topology.graph) - edge_data = [[(e[1], e[2]) => topology.graph[e[1], e[2]] for e in edge_labels(topology.graph)]; - [(e[2], e[1]) => topology.graph[e[1], e[2]] for e in edge_labels(topology.graph)]] - vertex_data = [l => topology.graph[l] for l in labels(topology.graph)] - underlying_graph = DiGraph(topology.graph.graph) - g = MetaGraph(underlying_graph, vertex_data, edge_data) + 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) @@ -66,25 +136,18 @@ function show_communication_data(topology::Topology, min_date = initial_time max_date = max([m.arriving_date for m in messages]...) - delta = _to_seconds(max_date, min_date) + delta = _to_seconds(max_date, min_date) + resolution_s sg = SliderGrid(fig[2, 1], - (label="Time", range=0:resolution_s:delta, format="{:.1f}", startvalue=0), + (label="Time", range=0:resolution_s:delta, format="{:.2f}", startvalue=0), tellheight=true) sliderobservable = sg.sliders[1].value - aid_to_node_id = Dict{String,Int}() - for label in labels(topology.graph) - node = topology.graph[label] - for agent in node.agents - aid_to_node_id[aid(agent)] = label - end - end 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) + 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]) @@ -99,7 +162,7 @@ function show_communication_data(topology::Topology, 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) + 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 @@ -114,7 +177,7 @@ function show_communication_data(topology::Topology, 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) + 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 @@ -129,7 +192,7 @@ function show_communication_data(topology::Topology, 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) + 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 @@ -144,7 +207,7 @@ function show_communication_data(topology::Topology, 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) + 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 @@ -159,7 +222,7 @@ function show_communication_data(topology::Topology, 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) + 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 @@ -170,18 +233,21 @@ function show_communication_data(topology::Topology, ew end - p = graphplot!(ax, g, layout=Shell(), + 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=:gray, + node_color=node_colors, node_strokewidth=0, arrow_size=24, arrow_shift=arrow_shifts, arrow_marker=arrow_markers, - ilabels=repr.(1:nv(g)), + ilabels=ilabels, ilabels_color=:white, elabels_attr=(word_wrap_width=5,)) @@ -209,10 +275,18 @@ function show_communication_data(topology::Topology, return fig end -function show_communication_data(topology::Topology, - world::World; +function show_communication_data(world::World; resolution_s::Real=0.1, - show::Bool=true) - return show_communication_data(topology, world.recorded_messages, world.clock.initial_time, - resolution_s=resolution_s, show=show) + 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/src/visualization/observation.jl b/src/visualization/observation.jl index 17c8727b..4d34e373 100644 --- a/src/visualization/observation.jl +++ b/src/visualization/observation.jl @@ -26,7 +26,7 @@ function plot_agents(world::World, recording::String; color=:viridis) data = data_agent_collection(world, recording) - ax = Axis(fig, + ax = Axis(fig[1,1], title="$recording over time for each agent", xlabel="time (seconds)", ylabel=recording, @@ -35,7 +35,7 @@ function plot_agents(world::World, recording::String; labels = [pair[1] for pair in pairs] values = [pair[2] for pair in pairs] series!(ax, data.time, hcat(values...)', labels=labels, color=color) - axislegend(ax, position=:lt) + Legend(fig[1, 2], ax) if !isnothing(write_to) save(write_to, fig) end @@ -51,23 +51,31 @@ end function plot_recordings(world::World; write_to::Union{Nothing,String}="observation.png", - size=(600, 600), + size=:auto, color=:black, colormap=:viridis) dc = world.data_collections dac = world.data_agent_collections + if size == :auto + size = ( + min(max(length(dc), length(dac)), 3) * 400, + 600 + ((length(dc)-1) ÷ 3 + (length(dac)-1) ÷ 3) * 250 + ) + end main_fig = Figure(size=size) world_layout = main_fig[1, 1] = GridLayout() agent_layout = main_fig[2, 1] = GridLayout() for (i, key) in enumerate(keys(dc)) - plot_world(world, key, write_to=nothing, fig=world_layout[1, i], color=color, colormap=colormap) + layout_fig = world_layout[((i - 1) ÷ 3) + 1, ((i - 1) % 3) + 1] + plot_world(world, key, write_to=nothing, fig=layout_fig, color=color, colormap=colormap) end _create_label(world_layout, "W") for (i, key) in enumerate(keys(dac)) - plot_agents(world, key, write_to=nothing, fig=agent_layout[1, i], color=colormap) + layout_fig = agent_layout[((i - 1) ÷ 3) + 1, ((i - 1) % 3) + 1] + plot_agents(world, key, write_to=nothing, fig=layout_fig, color=colormap) end _create_label(agent_layout, "A") diff --git a/test/visualization_tests.jl b/test/visualization_tests.jl index 44612111..987a82ba 100644 --- a/test/visualization_tests.jl +++ b/test/visualization_tests.jl @@ -68,7 +68,7 @@ end results = discrete_step_until(world, 1000) end - show_communication_data(topology, world, show=false) + show_communication_data(world, show=false, based_on=topology) rm("communication.svg") end diff --git a/test/world_tests.jl b/test/world_tests.jl index 0c86b9f6..d882486e 100644 --- a/test/world_tests.jl +++ b/test/world_tests.jl @@ -199,6 +199,66 @@ end @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=agent1.aid)) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + + 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=agent1.aid)) + send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + + 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 From e136d291e9cd5b626dad798ca49ce3eb11b26ef4 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Wed, 26 Mar 2025 15:29:48 +0100 Subject: [PATCH 38/54] Topology usage improvements. Adding aid_graph generation function for convenience. Adding descriptions for agents. --- src/agent/core.jl | 4 +- src/agent/role.jl | 16 +++++ src/simulation/communication.jl | 31 ++++++++-- src/simulation/world.jl | 30 ++++----- src/util/topology.jl | 102 ++++++++++++++++++++++++------- src/visualization/observation.jl | 59 ++++++++++++------ test/topology_tests.jl | 17 ++++++ 7 files changed, 201 insertions(+), 58 deletions(-) diff --git a/src/agent/core.jl b/src/agent/core.jl index 1d158c66..a8fc77f4 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -142,6 +142,7 @@ 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)) @@ -718,4 +719,5 @@ function dispatch_global_event(agent::Agent, event::Any) for role in roles(agent) on_global_event(role, event) end -end \ No newline at end of file +end + diff --git a/src/agent/role.jl b/src/agent/role.jl index 00d9fb51..8dd19916 100644 --- a/src/agent/role.jl +++ b/src/agent/role.jl @@ -287,6 +287,22 @@ 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 diff --git a/src/simulation/communication.jl b/src/simulation/communication.jl index c1dc51e9..03dd17b5 100644 --- a/src/simulation/communication.jl +++ b/src/simulation/communication.jl @@ -1,4 +1,5 @@ -export CommunicationSimulation, PackageResult, CommunicationSimulationResult, MessagePackage, calculate_communication, SimpleCommunicationSimulation +export CommunicationSimulation, PackageResult, CommunicationSimulationResult, MessagePackage, calculate_communication, + SimpleCommunicationSimulation, DelayProviderCommunicationSimulation using Dates @@ -54,9 +55,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 +67,28 @@ 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() +end + +function calculate_communication(communication_sim::DelayProviderCommunicationSimulation, clock::Clock, messages::Vector{MessagePackage})::CommunicationSimulationResult + results::Vector{PackageResult} = Vector() + for message in messages + 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 + push!(results, PackageResult(true, delay_s)) + end + return CommunicationSimulationResult(results) +end \ No newline at end of file diff --git a/src/simulation/world.jl b/src/simulation/world.jl index eb8daacf..4fc8abfa 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -80,6 +80,7 @@ An AgentsRecording is a container to record data of the agents. timeseries::Dict{String,Vector{Any}} = Dict() time::Vector{Real} = Vector() data::Any = nothing + dedicated_plots = false end struct MessageTransaction @@ -100,8 +101,8 @@ The World used as a base struct to enable simulations in Mango.jl. Always create task_sim::TaskSimulation = SimpleTaskSimulation(clock=clock) communication_sim::CommunicationSimulation = SimpleCommunicationSimulation() world_observer::WorldObserver = DispatchToAgentWorldObserver(container.agents) - data_collections::Dict{String,WorldRecording} = Dict() - data_agent_collections::Dict{String,AgentsRecording} = Dict() + data_collections::OrderedDict{String,WorldRecording} = OrderedDict() + data_agent_collections::OrderedDict{String,AgentsRecording} = OrderedDict() data_collectors::Vector{Function} = Vector() recorded_messages::Vector{MessageTransaction} = Vector() end @@ -342,7 +343,7 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT; max_adv comm_result = nothing if time_step_s == DISCRETE_EVENT time_step_s, comm_result = determine_time_step(world) - @info "Determined the size to be $time_step_s" + @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) @@ -388,7 +389,7 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT; max_adv world.clock.simulation_time = add_seconds(time(world), time_step_s) world.container.step_size_s = 0 - @info "New time" time(world) + @debug "New time" time(world) do_recordings(world) @@ -469,8 +470,8 @@ end Return the data collection with the `key` from the world. """ -function data_agent_collection(world::World, key::String) - return get!(world.data_agent_collections, key, AgentsRecording()) +function data_agent_collection(world::World, key::String; dedicated_plots::Bool=false) + return get!(world.data_agent_collections, key, AgentsRecording(dedicated_plots=dedicated_plots)) end """ @@ -491,8 +492,8 @@ 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) - dac = data_agent_collection(world, key) +function collect_agent_data(collector::Function, world::World, key::String; dedicated_plots::Bool=false) + dac = data_agent_collection(world, key, dedicated_plots=dedicated_plots) for agent in values(agents(world)) push!(world.data_collectors, () -> collector(world, agent, dac)) end @@ -519,9 +520,9 @@ 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) - collect_agent_data(world, key) do w, a, dc - insert_agent_recording!(dc, w, a, agent_recorder(a)) +function record_agent!(agent_recorder::Function, world::World, key::String; dedicated_plots::Bool=false) + collect_agent_data(world, key, dedicated_plots=dedicated_plots) do w, a, dc + insert_agent_recording!(dc, w, a, agent_recorder(a), dedicated_plots=dedicated_plots) end end @@ -530,11 +531,12 @@ function record_agent_having!(agent_recorder::Function, key::String, role_type::DataType; agent_color::Union{Nothing,Symbol}=nothing, - aid_contains::Union{Nothing,String}=nothing) + aid_contains::Union{Nothing,String}=nothing, + dedicated_plots::Bool=false) - collect_agent_data(world, key) do w, a, dc + collect_agent_data(world, key, dedicated_plots=dedicated_plots) do w, a, dc if has_role(a, role_type) && - (isnothing(color) || agent_color == color(a)) && + (isnothing(agent_color) || agent_color == color(a)) && (isnothing(aid_contains) || occursin(aid_contains, aid(a))) insert_agent_recording!(dc, w, a, agent_recorder(a)) diff --git a/src/util/topology.jl b/src/util/topology.jl index bc70c45c..64e96b29 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -1,7 +1,7 @@ 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!, - auto_assign!, topology_node_id + auto_assign!, topology_node_id, topology_to_aid_graph using MetaGraphsNext using Graphs @@ -13,6 +13,7 @@ import Graphs.add_edge! end struct Topology + tid::Symbol graph::MetaGraph end @@ -54,9 +55,9 @@ end Create a fully-connected topology. """ -function complete_topology(number_of_nodes::Int)::Topology +function complete_topology(number_of_nodes::Int, tid::Symbol=:default)::Topology graph = complete_graph(number_of_nodes) - return Topology(_create_meta_graph_with(graph)) + return Topology(tid, _create_meta_graph_with(graph)) end """ @@ -64,9 +65,9 @@ end Create a star topology. """ -function star_topology(number_of_nodes::Int) +function star_topology(number_of_nodes::Int, tid::Symbol=:default) graph = star_graph(number_of_nodes) - return Topology(_create_meta_graph_with(graph)) + return Topology(tid, _create_meta_graph_with(graph)) end """ @@ -74,9 +75,9 @@ end Create a cycle topology. """ -function cycle_topology(number_of_nodes::Int) +function cycle_topology(number_of_nodes::Int, tid::Symbol=:default) graph = cycle_graph(number_of_nodes) - return Topology(_create_meta_graph_with(graph)) + return Topology(tid, _create_meta_graph_with(graph)) end """ @@ -84,8 +85,8 @@ end Create a topology based on a Graphs.jl (abstract) graph. """ -function graph_topology(graph::AbstractGraph) - return Topology(_create_meta_graph_with(graph)) +function graph_topology(graph::AbstractGraph, tid::Symbol=:default) + return Topology(tid, _create_meta_graph_with(graph)) end """ @@ -137,11 +138,16 @@ end 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) +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_neighborhoods_and_inject(topology::Topology, tid::Symbol=:default) +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] @@ -153,9 +159,17 @@ function _build_neighborhoods_and_inject(topology::Topology, tid::Symbol=:defaul append!(neighbor_addresses, [address(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) + neighbor_addresses = get!(state_to_same, NORMAL, Vector()) + push!(neighbor_addresses, address(other_agent)) + end + end topology_service = service_of_type(agent, TopologyService, TopologyService()) - topology_service.tid_to_state_to_neighbors[tid] = state_to_neighbors - topology_service.tid_to_node_id[tid] = node.id + topology_service.tid_to_state_to_neighbors[topology.tid] = state_to_same + topology_service.tid_to_node_id[topology.tid] = node.id end end end @@ -181,9 +195,9 @@ end ``` """ function create_topology(create_runnable::Function; tid::Symbol=:default, directed::Bool=false) - topology = Topology(_create_meta_graph_with(directed ? DiGraph() : Graph())) + topology = Topology(tid, _create_meta_graph_with(directed ? DiGraph() : Graph())) create_runnable(topology) - _build_neighborhoods_and_inject(topology, tid) + _build_neighborhoods_and_inject(topology) return topology end @@ -207,9 +221,9 @@ modify_topology(my_topology) do topology end ``` """ -function modify_topology(modify_runnable::Function, topology::Topology; tid::Symbol=:default) +function modify_topology(modify_runnable::Function, topology::Topology) modify_runnable(topology) - _build_neighborhoods_and_inject(topology, tid) + _build_neighborhoods_and_inject(topology) return topology end @@ -226,13 +240,13 @@ per_node(topology) do node end ``` """ -function per_node(assign_runnable::Function, topology::Topology; tid::Symbol=:default) +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, tid) + _build_neighborhoods_and_inject(topology) end """ @@ -241,14 +255,14 @@ end 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; tid::Symbol=:default) +function auto_assign!(topology::Topology, container::ContainerInterface) index_to_label = collect(labels(topology.graph)) for (i, agent) in enumerate(agents(container)) label = index_to_label[(((i-1)%length(index_to_label))+1)] node = topology.graph[label] add!(node, agent) end - _build_neighborhoods_and_inject(topology, tid) + _build_neighborhoods_and_inject(topology) end """ @@ -359,3 +373,49 @@ 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 !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), graph +end diff --git a/src/visualization/observation.jl b/src/visualization/observation.jl index 4d34e373..fadd3ec2 100644 --- a/src/visualization/observation.jl +++ b/src/visualization/observation.jl @@ -26,23 +26,34 @@ function plot_agents(world::World, recording::String; color=:viridis) data = data_agent_collection(world, recording) - ax = Axis(fig[1,1], - title="$recording over time for each agent", - xlabel="time (seconds)", - ylabel=recording, - ) pairs = collect(data.timeseries) labels = [pair[1] for pair in pairs] values = [pair[2] for pair in pairs] - series!(ax, data.time, hcat(values...)', labels=labels, color=color) - Legend(fig[1, 2], ax) + 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) - return Label(layout[1, 1, TopLeft()], label, +function _create_label(layout, label, y) + return Label(layout[y, 1, TopLeft()], label, fontsize=26, font=:bold, padding=(0, 5, 5, 0), @@ -55,29 +66,41 @@ function plot_recordings(world::World; color=:black, colormap=:viridis) + row_length = 3 dc = world.data_collections dac = world.data_agent_collections if size == :auto + dac_length = sum([record.dedicated_plots ? length(record.timeseries) : 1 for record in values(dac)]) size = ( - min(max(length(dc), length(dac)), 3) * 400, - 600 + ((length(dc)-1) ÷ 3 + (length(dac)-1) ÷ 3) * 250 + min(max(length(dc), dac_length), 3) * 400, + 600 + ((length(dc)-1) ÷ 3 + (dac_length-1) ÷ 3) * 250 ) end main_fig = Figure(size=size) - world_layout = main_fig[1, 1] = GridLayout() - agent_layout = main_fig[2, 1] = GridLayout() + all_layout = main_fig[1, 1] = GridLayout() + _create_label(all_layout, "W", 1) for (i, key) in enumerate(keys(dc)) - layout_fig = world_layout[((i - 1) ÷ 3) + 1, ((i - 1) % 3) + 1] + 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 - _create_label(world_layout, "W") + 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 = agent_layout[((i - 1) ÷ 3) + 1, ((i - 1) % 3) + 1] - plot_agents(world, key, write_to=nothing, fig=layout_fig, color=colormap) + 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 - _create_label(agent_layout, "A") if !isnothing(write_to) return save(write_to, main_fig) diff --git a/test/topology_tests.jl b/test/topology_tests.jl index 048de7bc..fb372612 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 From fc16efb8acf2886ead40e9d287dc5961405b26c8 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Mon, 7 Apr 2025 17:40:11 +0200 Subject: [PATCH 39/54] Added topology connection feature. --- Project.toml | 4 +- src/agent/core.jl | 2 +- src/container/api.jl | 7 -- src/simulation/communication.jl | 53 +++++++++++++- src/simulation/world.jl | 4 +- src/util/topology.jl | 122 +++++++++++++++++++++++++------- 6 files changed, 154 insertions(+), 38 deletions(-) diff --git a/Project.toml b/Project.toml index 8b8eaa58..7dea2cc2 100644 --- a/Project.toml +++ b/Project.toml @@ -10,6 +10,7 @@ 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" GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" @@ -28,6 +29,7 @@ CairoMakie = "0.13.1" Colors = "~0.12" ConcurrentCollections = "~0.1" ConcurrentUtilities = "~2.2" +Distributions = "~0.25" GLMakie = "0.11.2" GraphMakie = "0.5.13" Graphs = "~1.10" @@ -41,9 +43,9 @@ Parameters = "~0.12" julia = "^1.9" [extras] -GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] diff --git a/src/agent/core.jl b/src/agent/core.jl index a8fc77f4..4900ce50 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -324,7 +324,7 @@ function color(agent::Agent) return description(agent).color end -function update_description(agent::Agent; color=nothing, name=nothing, category=nothing) +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 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/simulation/communication.jl b/src/simulation/communication.jl index 03dd17b5..d280ce5a 100644 --- a/src/simulation/communication.jl +++ b/src/simulation/communication.jl @@ -1,7 +1,10 @@ export CommunicationSimulation, PackageResult, CommunicationSimulationResult, MessagePackage, calculate_communication, - SimpleCommunicationSimulation, DelayProviderCommunicationSimulation + SimpleCommunicationSimulation, DelayProviderCommunicationSimulation, create_distribution_based_com_sim using Dates +using Graphs +using MetaGraphsNext +using Distributions """ Interface to implement a communication simulation. @@ -37,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(...)")) @@ -78,17 +84,58 @@ 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 - push!(results, PackageResult(true, delay_s)) + 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::Real=1, + base_delay_per_message::Real=20, + 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 + distmatrix[to, from] = default_delay_per_edge + end + default_distr = distribution_provider(base_delay_per_message) + 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) + specific_distr = distribution_provider(base_delay_per_message + distance) + provider_com.delay_s_directed_edge_dict[(aid(agent), label_other)] = () -> 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/world.jl b/src/simulation/world.jl index 4fc8abfa..ff401f79 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -254,7 +254,7 @@ 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 @@ -522,7 +522,7 @@ it in the data collection with the `key`. The data can be plotted using plot_age """ function record_agent!(agent_recorder::Function, world::World, key::String; dedicated_plots::Bool=false) collect_agent_data(world, key, dedicated_plots=dedicated_plots) do w, a, dc - insert_agent_recording!(dc, w, a, agent_recorder(a), dedicated_plots=dedicated_plots) + insert_agent_recording!(dc, w, a, agent_recorder(a)) end end diff --git a/src/util/topology.jl b/src/util/topology.jl index 64e96b29..3b58f35b 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -1,7 +1,7 @@ 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!, - auto_assign!, topology_node_id, topology_to_aid_graph + auto_assign!, topology_node_id, topology_to_aid_graph, set_as_connector!, connect_topologies!, mark_as_connector! using MetaGraphsNext using Graphs @@ -12,9 +12,11 @@ import Graphs.add_edge! agents::Vector{Agent} = Vector() end -struct Topology +@kwdef struct Topology tid::Symbol graph::MetaGraph + connectors::Vector{Tuple{Symbol,AgentAddress}} = Vector() # connection type to connector + connections::Vector{Tuple{Symbol,Topology}} = Vector() # tid to connection type end @enum State begin @@ -25,8 +27,10 @@ end end @kwdef mutable struct TopologyService - tid_to_state_to_neighbors::Dict{Symbol,Dict{State,Vector{AgentAddress}}} = Dict() - tid_to_node_id::Dict{Symbol,Int} = Dict() + tid_to_state_to_neighbors::Dict{Symbol,Dict{State,Vector{AgentAddress}}} = Dict() # tid to (edge state to agents) + tid_to_connectors::Dict{Symbol,Vector{Tuple{Symbol, AgentAddress}}} = 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() end function service_node_id(service::TopologyService, tid::Symbol=:default) @@ -36,9 +40,10 @@ function service_node_id(service::TopologyService, tid::Symbol=:default) return service.tid_to_node_id[tid] end -function neighbors(service::TopologyService, tid::Symbol=:default, state::State=NORMAL) +function neighbors(service::TopologyService, tid::Symbol=:default, state::State=NORMAL; include_connectors::Vector{Symbol}=Vector{Symbol}()) if haskey(service.tid_to_state_to_neighbors, tid) - return get(service.tid_to_state_to_neighbors[tid], state, Vector()) + return vcat(get(service.tid_to_state_to_neighbors[tid], state, Vector()), + [t[2] for t in service.tid_to_connectors[tid] if t[1] in include_connectors]) end throw(ArgumentError("No neighbors found for tid=$tid")) end @@ -57,7 +62,7 @@ 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, _create_meta_graph_with(graph)) + return Topology(tid=tid, graph=_create_meta_graph_with(graph)) end """ @@ -67,7 +72,7 @@ Create a star topology. """ function star_topology(number_of_nodes::Int, tid::Symbol=:default) graph = star_graph(number_of_nodes) - return Topology(tid, _create_meta_graph_with(graph)) + return Topology(tid=tid, graph=_create_meta_graph_with(graph)) end """ @@ -77,7 +82,7 @@ Create a cycle topology. """ function cycle_topology(number_of_nodes::Int, tid::Symbol=:default) graph = cycle_graph(number_of_nodes) - return Topology(tid, _create_meta_graph_with(graph)) + return Topology(tid=tid, graph=_create_meta_graph_with(graph)) end """ @@ -86,7 +91,7 @@ end Create a topology based on a Graphs.jl (abstract) graph. """ function graph_topology(graph::AbstractGraph, tid::Symbol=:default) - return Topology(tid, _create_meta_graph_with(graph)) + return Topology(tid=tid, graph=_create_meta_graph_with(graph)) end """ @@ -133,6 +138,39 @@ function add_node!(topology::Topology, agents::Agent...; id::Union{Int,Nothing}= 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, address(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) @@ -147,6 +185,25 @@ function set_edge_state!(topology::Topology, node_id_from::Int, node_id_to::Int, 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, addr) in topology.connectors + if type == c_type && address(agent) == addr + # it is a connector + # now find the fitting connectors in the connected topo + for (other_c_type, other_addr) in other_topo.connectors + if type == other_c_type + push!(connectors_for_agent, (type, other_addr)) + end + end + end + end + end + return connectors_for_agent +end + function _build_neighborhoods_and_inject(topology::Topology) # 2nd pass, build the neighborhoods and add it to agents for label in labels(topology.graph) @@ -163,13 +220,23 @@ function _build_neighborhoods_and_inject(topology::Topology) state_to_same = deepcopy(state_to_neighbors) for other_agent in node.agents if aid(agent) != aid(other_agent) - neighbor_addresses = get!(state_to_same, NORMAL, Vector()) + neighbor_addresses = get!(state_to_same, NORMAL, Vector()) push!(neighbor_addresses, address(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 + + # look for marks and transfer to topology + for type in topology_service.marked_connector_for + push!(topology.connectors, (type, address(agent))) + end + empty!(topology_service.marked_connector_for) + + # 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 end @@ -195,7 +262,7 @@ end ``` """ function create_topology(create_runnable::Function; tid::Symbol=:default, directed::Bool=false) - topology = Topology(tid, _create_meta_graph_with(directed ? DiGraph() : Graph())) + topology = Topology(tid=tid, graph=_create_meta_graph_with(directed ? DiGraph() : Graph())) create_runnable(topology) _build_neighborhoods_and_inject(topology) return topology @@ -247,22 +314,29 @@ function per_node(assign_runnable::Function, topology::Topology) assign_runnable(node) 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) +function auto_assign!(topology::Topology, agents) index_to_label = collect(labels(topology.graph)) - for (i, agent) in enumerate(agents(container)) + 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 """ @@ -312,12 +386,12 @@ end 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)::Vector{AgentAddress} - return neighbors(service_of_type(agent, TopologyService, TopologyService()), tid, state) +function topology_neighbors(agent::Agent; tid::Symbol=:default, state::State=NORMAL, include_connectors::Vector{Symbol}=Vector{Symbol}())::Vector{AgentAddress} + return neighbors(service_of_type(agent, TopologyService, TopologyService()), tid, state, include_connectors=include_connectors) end -function topology_neighbors(role::Role; tid::Symbol=:default, state::State=NORMAL)::Vector{AgentAddress} - return neighbors(service_of_type(role.context.agent, TopologyService, TopologyService()), tid, state) +function topology_neighbors(role::Role; tid::Symbol=:default, state::State=NORMAL, include_connectors::Vector{Symbol}=Vector{Symbol}())::Vector{AgentAddress} + return neighbors(service_of_type(role.context.agent, TopologyService, TopologyService()), tid, state, include_connectors=include_connectors) end """ @@ -417,5 +491,5 @@ function topology_to_aid_graph(topology::Topology) end end end - return MetaGraph(graph, vertex_description, edges_description), graph + return MetaGraph(graph, vertex_description, edges_description) end From 80e153cc48066bf2e200297fc428f398a36c1831 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Mon, 7 Apr 2025 18:14:05 +0200 Subject: [PATCH 40/54] Documenting the new topology features. --- docs/src/api.md | 18 ++++++++++++++---- docs/src/topology.md | 31 +++++++++++++++++++++++++------ src/util/topology.jl | 6 +++--- 3 files changed, 42 insertions(+), 13 deletions(-) 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/topology.md b/docs/src/topology.md index a171d28b..61a23328 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,11 @@ 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 @@ -70,3 +81,11 @@ 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`. + +# Connecting topologies together + +Sometimes systems become so complex that creating multiple simple topologies is easier than creating one complex topology. If you use more than only one topology, you can connect your topologies together to be linked on so-called `connectors`. + +`Connectors` are single agents, which 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 connector you can use [`mark_as_connector!`](@ref). + +If you connect two topologies, say 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 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. diff --git a/src/util/topology.jl b/src/util/topology.jl index 3b58f35b..271cb8a8 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -1,6 +1,6 @@ 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!, + 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! using MetaGraphsNext @@ -357,7 +357,7 @@ Assign all agents of the `container` to the nodes based on the given `assign_con 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) +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) @@ -373,7 +373,7 @@ end 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) +function choose_agents!(choose_agent_function::Function, topology::Topology) per_node(topology) do node agent = choose_agent_function(node) add!(node, agent) From 52255d32daa98458ad85a9d766398ec71483bfc4 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 15 Apr 2025 15:27:51 +0200 Subject: [PATCH 41/54] Improving topology documentation. --- docs/src/topology.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/topology.md b/docs/src/topology.md index 61a23328..781e4dd6 100644 --- a/docs/src/topology.md +++ b/docs/src/topology.md @@ -88,4 +88,4 @@ Sometimes systems become so complex that creating multiple simple topologies is `Connectors` are single agents, which 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 connector you can use [`mark_as_connector!`](@ref). -If you connect two topologies, say 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 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. +If you connect two topologies, say 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 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 From a434cdaf6a58821515b9df43fe0447af39a3c14e Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 15 Apr 2025 15:29:42 +0200 Subject: [PATCH 42/54] Fixing topology tests, broken due to renaming. --- test/topology_tests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/topology_tests.jl b/test/topology_tests.jl index fb372612..2ae6401b 100644 --- a/test/topology_tests.jl +++ b/test/topology_tests.jl @@ -159,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 @@ -177,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 From 391958d2604cc3ec78637927917075e064df8693 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Fri, 30 May 2025 13:06:27 +0200 Subject: [PATCH 43/54] Adding system programming feature. Added some minor improvements. --- src/agent/api.jl | 10 +++- src/agent/core.jl | 88 +++++++++++++++++++++++++++--- src/agent/role.jl | 4 +- src/express/api.jl | 64 +++++++++++++++++++++- src/simulation/communication.jl | 4 ++ src/util/topology.jl | 63 ++++++++++++++++++--- src/visualization/communication.jl | 80 ++++++++++++++++++++++++++- test/runtests.jl | 1 + test/system_programming_tests.jl | 32 +++++++++++ test/visualization_tests.jl | 27 ++++++++- 10 files changed, 349 insertions(+), 24 deletions(-) create mode 100644 test/system_programming_tests.jl 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 4900ce50..53b2b079 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -1,6 +1,7 @@ export @agent, AgentContext, AgentRoleHandler, + SystemHandler, handle_message, handle_unanswered, add, @@ -52,7 +53,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} @@ -70,6 +71,12 @@ mutable struct AgentDescription color::Symbol end +struct SystemHandler + message_subs::Vector{Tuple{Function,Function}} + event_subs::Dict{Any,Vector{Tuple{Function,Function}}} + global_event_subs::Vector{Tuple{Function,Function}} +end + """ All baseline fields added by the @agent macro are listed in this vector. They are added in the same order defined here. @@ -77,7 +84,8 @@ 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}()), @@ -177,6 +185,28 @@ function handle_transaction_message(agent::Agent, message::Any, meta::AbstractDi end end +@kwdef struct WaitingMessagePreprocessor <: MessagePreprocessor + waiting_for_func::Function + waiting::Dict{AgentAddress,Bool} = Dict() +end + +function init(preprocessor::WaitingMessagePreprocessor, role::Role) + for addr in preprocessor.waiting_for_func() + preprocessor.waiting[addr] = true + end +end + +function handle(preprocessor::WaitingMessagePreprocessor, role::Role, handler::Function, message::Any, meta::AbstractDict) + sender = sender_addr(meta) + if sender in preprocessor.waiting + preprocessor.waiting[sender] = false + end + if !any(preprocessor.waiting) + init(preprocessor, role) + handler(role, 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 @@ -205,18 +235,32 @@ function dispatch_message(agent::Agent, message::Any, meta::AbstractDict) lock(agent.lock) do # check if part of a transaction - if haskey(meta, TRACKING_ID) && haskey(agent.transaction_handler, meta[TRACKING_ID]) + 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) in agent.system_handler.message_subs + if condition(message, meta) + call(agent, message, meta) + end + end end if length(agent.outgoing) < 1 for role in agent.role_handler.roles @@ -385,9 +429,13 @@ function subscribe_message_handle( agent::Agent, role::Role, condition::Function, - handler::Function, + handler::Function; + preprocessor::Union{Nothing,MessagePreprocessor}=nothing, ) - push!(agent.role_handler.handle_message_subs, (role, condition, handler)) + if !isnothing(preprocessor) + init(preprocessor, role) + end + push!(agent.role_handler.handle_message_subs, (role, condition, handler, preprocessor)) end function subscribe_send_handle(agent::Agent, role::Role, handler::Function) @@ -413,6 +461,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 (role, condition, func) in agent.system_handler.event_subs[key] + if condition(src, event) + func(role, src, event, event_type) + end + end + end end function get_model_handle(agent::Agent, type::DataType) @@ -719,5 +774,22 @@ function dispatch_global_event(agent::Agent, event::Any) for role in roles(agent) on_global_event(role, event) end + for (condition, call) in agent.system_handler.global_event_subs + if condition(event) + call(agent, event) + end + end +end + +function _add_system_handle_message_sub(agent::Agent, filter::Function, handle::Function) + push!(agent.system_handler.message_subs, (filter, handle)) +end + +function _add_system_event_sub(agent::Agent, 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)) end +function _add_system_global_event_sub(agent::Agent, filter::Function, handle::Function) + push!(agent.system_handler.global_event_subs, (filter, handle)) +end \ No newline at end of file diff --git a/src/agent/role.jl b/src/agent/role.jl index 8dd19916..5835c2c7 100644 --- a/src/agent/role.jl +++ b/src/agent/role.jl @@ -209,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 """ diff --git a/src/express/api.jl b/src/express/api.jl index 0a15087d..687bc05d 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) @@ -338,4 +338,64 @@ 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}(), + has_roles::Union{Vector{DataType},DataType}=Vector{DataType}(), + match_names::Union{Vector{String},String}=Vector{String}(), + match_colors::Union{Vector{String},String}=Vector{String}()) + + if !(agent_types isa Vector) + agent_types = [agent_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) && length(match_names) && length(match_colors))] + for agent in filtered_agents + if !isnothing(on_message) + _add_system_handle_message_sub(agent, (msg, meta) -> typeof(msg) == on_message, func) + end + if !isnothing(on_event) + _add_system_event_sub(agent, on_event, (src, event) -> typeof(event) == on_event, func) + end + if !isnothing(on_global_event) + _add_system_global_event_sub(agent, (event) -> typeof(event) == on_global_event, func) + end + end +end diff --git a/src/simulation/communication.jl b/src/simulation/communication.jl index d280ce5a..0d90f947 100644 --- a/src/simulation/communication.jl +++ b/src/simulation/communication.jl @@ -110,6 +110,7 @@ function create_distribution_based_com_sim(aid_graph::MetaGraph, agents::Vector{Agent}; default_delay_per_edge::Real=1, base_delay_per_message::Real=20, + max_edge_delay::Real=100, distribution_provider::Function=(delay) -> Poisson(delay), label_replacer::Function=(label) -> label)::DelayProviderCommunicationSimulation @@ -132,6 +133,9 @@ function create_distribution_based_com_sim(aid_graph::MetaGraph, 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 + distance) provider_com.delay_s_directed_edge_dict[(aid(agent), label_other)] = () -> 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)] diff --git a/src/util/topology.jl b/src/util/topology.jl index 271cb8a8..8af00321 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -1,7 +1,8 @@ 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! + auto_assign!, topology_node_id, topology_to_aid_graph, set_as_connector!, connect_topologies!, mark_as_connector!, + topology_connectors, topology_connection_types using MetaGraphsNext using Graphs @@ -24,6 +25,7 @@ end 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 @@ -48,6 +50,20 @@ function neighbors(service::TopologyService, tid::Symbol=:default, state::State= throw(ArgumentError("No neighbors found for tid=$tid")) end +function connectors(service::TopologyService, tid::Symbol=:default; include_connectors::Vector{Symbol}=Vector{Symbol}()) + if haskey(service.tid_to_state_to_neighbors, tid) + return [t[2] for t in service.tid_to_connectors[tid] if t[1] in include_connectors || length(include_connectors) == 0] + 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 _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)] @@ -60,7 +76,7 @@ end Create a fully-connected topology. """ -function complete_topology(number_of_nodes::Int, tid::Symbol=:default)::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 @@ -70,7 +86,7 @@ end Create a star topology. """ -function star_topology(number_of_nodes::Int, tid::Symbol=:default) +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 @@ -80,7 +96,7 @@ end Create a cycle topology. """ -function cycle_topology(number_of_nodes::Int, tid::Symbol=:default) +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 @@ -90,7 +106,7 @@ end Create a topology based on a Graphs.jl (abstract) graph. """ -function graph_topology(graph::AbstractGraph, tid::Symbol=:default) +function graph_topology(graph::AbstractGraph; tid::Symbol=:default) return Topology(tid=tid, graph=_create_meta_graph_with(graph)) end @@ -230,9 +246,10 @@ function _build_neighborhoods_and_inject(topology::Topology) # look for marks and transfer to topology for type in topology_service.marked_connector_for - push!(topology.connectors, (type, address(agent))) + if !((type, address(agent)) in topology.connectors) + push!(topology.connectors, (type, address(agent))) + end end - empty!(topology_service.marked_connector_for) # search for connection agents connectors_for_agent = _build_connectors_list_for(topology, agent) @@ -407,6 +424,35 @@ 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}())::Vector{AgentAddress} + return connectors(service_of_type(agent, TopologyService, TopologyService()), tid, include_connectors=include_connectors) +end + +function topology_connectors(role::Role; tid::Symbol=:default, include_connectors::Vector{Symbol}=Vector{Symbol}())::Vector{AgentAddress} + return connectors(service_of_type(role.context.agent, TopologyService, TopologyService()), tid, include_connectors=include_connectors) +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 + # Graphs API calls forwarded to Topology function Graphs.edges(topology::Topology) return edges(topology.graph) @@ -468,6 +514,9 @@ function topology_to_aid_graph(topology::Topology) 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) diff --git a/src/visualization/communication.jl b/src/visualization/communication.jl index d2d2339b..2c223daf 100644 --- a/src/visualization/communication.jl +++ b/src/visualization/communication.jl @@ -1,4 +1,4 @@ -export plot_topology, show_communication_data +export plot_node_topology, plot_multi_agent_topology, show_communication_data using Makie using GraphMakie.NetworkLayout @@ -6,7 +6,7 @@ using GraphMakie using Graphs using Dates -function plot_topology(topology::Topology; write_to::Union{Nothing,String}="topology.svg", ax=nothing, fig=Figure()) +function plot_node_topology(topology::Topology; write_to::Union{Nothing,String}="topology.svg", ax=nothing, fig=Figure()) if isnothing(ax) ax = Axis(fig[1, 1]) @@ -30,6 +30,82 @@ function plot_topology(topology::Topology; write_to::Union{Nothing,String}="topo 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 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.aid)-$offset_i" + aid_s = "$(other_connector.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 diff --git a/test/runtests.jl b/test/runtests.jl index 6c7faa99..ecde33c0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,6 +2,7 @@ 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") diff --git a/test/system_programming_tests.jl b/test/system_programming_tests.jl new file mode 100644 index 00000000..b2c8bf58 --- /dev/null +++ b/test/system_programming_tests.jl @@ -0,0 +1,32 @@ + +using Mango +using Dates + +@agent struct SystemProgrammingAgent + 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 \ No newline at end of file diff --git a/test/visualization_tests.jl b/test/visualization_tests.jl index 987a82ba..6e41ede7 100644 --- a/test/visualization_tests.jl +++ b/test/visualization_tests.jl @@ -82,6 +82,31 @@ end topology = complete_topology(3) auto_assign!(topology, world) - plot_topology(topology, write_to="test_topology_plot.svg") + 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 == 13348 rm("test_topology_plot.svg") end \ No newline at end of file From e15ef67c6d45a770f7e5fb2eb164b66b1ed474fe Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Mon, 9 Jun 2025 18:53:19 +0200 Subject: [PATCH 44/54] Refactored visu API to modules. --- Project.toml | 15 ++-- .../MangoGraphVisualization.jl | 6 ++ .../src}/communication.jl | 13 +-- .../MangoPlotVisualization.jl | 6 ++ .../src}/observation.jl | 8 +- src/Mango.jl | 3 +- src/agent/core.jl | 66 ++++++++++------ src/express/api.jl | 36 +++++++-- src/simulation/world.jl | 2 +- src/util/scheduling.jl | 32 +++++++- src/util/topology.jl | 2 +- src/visualization.jl | 8 ++ test/agent_tests.jl | 79 +++++++++++++++++++ test/system_programming_tests.jl | 66 ++++++++++++++++ test/visualization_tests.jl | 2 + 15 files changed, 294 insertions(+), 50 deletions(-) create mode 100644 ext/MangoGraphVisualization/MangoGraphVisualization.jl rename {src/visualization => ext/MangoGraphVisualization/src}/communication.jl (95%) create mode 100644 ext/MangoPlotVisualization/MangoPlotVisualization.jl rename {src/visualization => ext/MangoPlotVisualization/src}/observation.jl (94%) create mode 100644 src/visualization.jl diff --git a/Project.toml b/Project.toml index 7dea2cc2..da3fc84f 100644 --- a/Project.toml +++ b/Project.toml @@ -11,12 +11,10 @@ ConcurrentUtilities = "f0e56b4a-5159-44fe-b623-3e5288b988bb" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" -GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LightBSON = "a4a7f996-b3a6-4de6-b9db-2fa5f350df41" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" -Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" Mosquitto = "db317de6-444b-4dfa-9d0e-fbf3d8dd78ea" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" @@ -30,7 +28,6 @@ Colors = "~0.12" ConcurrentCollections = "~0.1" ConcurrentUtilities = "~2.2" Distributions = "~0.25" -GLMakie = "0.11.2" GraphMakie = "0.5.13" Graphs = "~1.10" JSON = "~0.21" @@ -42,11 +39,19 @@ OrderedCollections = "~1.6" Parameters = "~0.12" julia = "^1.9" +[weakdeps] +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" + +[extensions] +MangoPlotVisualization = ["Makie"] +MangoGraphVisualization = ["Makie", "GraphMakie"] + [extras] CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" [targets] -test = ["Test", "Documenter", "CairoMakie"] +test = ["Test", "Documenter", "CairoMakie", "Makie", "GraphMakie"] 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/src/visualization/communication.jl b/ext/MangoGraphVisualization/src/communication.jl similarity index 95% rename from src/visualization/communication.jl rename to ext/MangoGraphVisualization/src/communication.jl index 2c223daf..b9564ac0 100644 --- a/src/visualization/communication.jl +++ b/ext/MangoGraphVisualization/src/communication.jl @@ -1,12 +1,13 @@ -export plot_node_topology, plot_multi_agent_topology, show_communication_data +using Mango using Makie using GraphMakie.NetworkLayout using GraphMakie using Graphs using Dates +using MetaGraphsNext -function plot_node_topology(topology::Topology; write_to::Union{Nothing,String}="topology.svg", ax=nothing, fig=Figure()) +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]) @@ -60,7 +61,7 @@ function combine_meta_graphs(graphs::Vector{<:MetaGraph}) return MetaGraph(combine_simple_graphs(graphs), vertices_description, edges_description) end -function plot_multi_agent_topology(topologies::Vector{Topology}; write_to::Union{Nothing,String}="multi_topology.svg") +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 @@ -123,7 +124,7 @@ end proxy_aid::String end -function aid(agent::VisuProxyAgent) +function Mango.aid(agent::VisuProxyAgent) return agent.proxy_aid end @@ -148,7 +149,7 @@ function _create_aid_based_data(g, nid, aid_to_x, default) return "$label" end -function show_communication_data(messages::Vector{MessageTransaction}, +function Mango.show_communication_data(messages::Vector{MessageTransaction}, initial_time::DateTime=DateTime(0); resolution_s::Real=0.1, show::Bool=true, @@ -351,7 +352,7 @@ function show_communication_data(messages::Vector{MessageTransaction}, return fig end -function show_communication_data(world::World; +function Mango.show_communication_data(world::World; resolution_s::Real=0.1, show::Bool=true, based_on::Union{Nothing,MetaGraph,Topology}=nothing) 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/src/visualization/observation.jl b/ext/MangoPlotVisualization/src/observation.jl similarity index 94% rename from src/visualization/observation.jl rename to ext/MangoPlotVisualization/src/observation.jl index fadd3ec2..01f026a9 100644 --- a/src/visualization/observation.jl +++ b/ext/MangoPlotVisualization/src/observation.jl @@ -1,8 +1,8 @@ -export plot_world, plot_agents, plot_recordings +using Mango using Makie -function plot_world(world::World, recording::String; +function Mango.plot_world(world::World, recording::String; write_to::Union{Nothing,String}="world_observation.png", fig=Figure(), color=:black, @@ -20,7 +20,7 @@ function plot_world(world::World, recording::String; end end -function plot_agents(world::World, recording::String; +function Mango.plot_agents(world::World, recording::String; write_to::Union{Nothing,String}="agent_observation.png", fig=Figure(), color=:viridis) @@ -60,7 +60,7 @@ function _create_label(layout, label, y) halign=:left) end -function plot_recordings(world::World; +function Mango.plot_recordings(world::World; write_to::Union{Nothing,String}="observation.png", size=:auto, color=:black, diff --git a/src/Mango.jl b/src/Mango.jl index d6858727..ec35f4b9 100644 --- a/src/Mango.jl +++ b/src/Mango.jl @@ -27,9 +27,8 @@ include("container/core.jl") include("simulation/container.jl") include("simulation/world.jl") include("util/topology.jl") -include("visualization/communication.jl") -include("visualization/observation.jl") include("express/api.jl") +include("visualization.jl") end # module diff --git a/src/agent/core.jl b/src/agent/core.jl index 53b2b079..84fecb1f 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -72,9 +72,9 @@ mutable struct AgentDescription end struct SystemHandler - message_subs::Vector{Tuple{Function,Function}} - event_subs::Dict{Any,Vector{Tuple{Function,Function}}} - global_event_subs::Vector{Tuple{Function,Function}} + 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 """ @@ -190,20 +190,20 @@ end waiting::Dict{AgentAddress,Bool} = Dict() end -function init(preprocessor::WaitingMessagePreprocessor, role::Role) +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::Role, handler::Function, message::Any, meta::AbstractDict) - sender = sender_addr(meta) - if sender in preprocessor.waiting +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(preprocessor.waiting) - init(preprocessor, role) - handler(role, message, meta) + end + if !any(values(preprocessor.waiting)) + init(preprocessor, role_or_agent) + handler(role_or_agent, message, meta) end end @@ -256,9 +256,15 @@ function dispatch_message(agent::Agent, message::Any, meta::AbstractDict) end end handle_message(agent, message, meta) - for (condition, call) in agent.system_handler.message_subs - if condition(message, meta) - call(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 @@ -438,6 +444,18 @@ function subscribe_message_handle( 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, +) + 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) push!(agent.role_handler.send_message_subs, (role, handler)) end @@ -462,9 +480,9 @@ function emit_event_handle(agent::Agent, src::Role, event::Any; event_type::Any= handle_event(role, src, event, event_type=event_type) end if haskey(agent.system_handler.event_subs, key) - for (role, condition, func) in agent.system_handler.event_subs[key] + for (condition, func, caller) in agent.system_handler.event_subs[key] if condition(src, event) - func(role, src, event, event_type) + func(caller, src, event, event_type) end end end @@ -774,22 +792,22 @@ function dispatch_global_event(agent::Agent, event::Any) for role in roles(agent) on_global_event(role, event) end - for (condition, call) in agent.system_handler.global_event_subs + for (condition, call, caller) in agent.system_handler.global_event_subs if condition(event) - call(agent, event) + call(caller, event) end end end -function _add_system_handle_message_sub(agent::Agent, filter::Function, handle::Function) - push!(agent.system_handler.message_subs, (filter, handle)) +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, event_type::Any, filter::Function, handle::Function) +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)) + push!(event_type_subs, (filter, handle, caller)) end -function _add_system_global_event_sub(agent::Agent, filter::Function, handle::Function) - push!(agent.system_handler.global_event_subs, (filter, handle)) +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/express/api.jl b/src/express/api.jl index 687bc05d..a35206a3 100644 --- a/src/express/api.jl +++ b/src/express/api.jl @@ -365,13 +365,18 @@ function behavior_in(func::Function, world::World; 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}()) + 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 @@ -386,16 +391,35 @@ function behavior_in(func::Function, world::World; (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) && length(match_names) && length(match_colors))] - for agent in filtered_agents + (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, (msg, meta) -> typeof(msg) == on_message, func) + _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, on_event, (src, event) -> typeof(event) == on_event, func) + _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, (event) -> typeof(event) == on_global_event, func) + _add_system_global_event_sub(agent, caller, (event) -> typeof(event) == on_global_event, func) end end end diff --git a/src/simulation/world.jl b/src/simulation/world.jl index ff401f79..ee1af6b4 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -1,7 +1,7 @@ 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! + record_world!, record_agent!, record_agent_having!, MessageTransaction, data_collection, data_agent_collection using Base.Threads using Dates diff --git a/src/util/scheduling.jl b/src/util/scheduling.jl index 786bd0c0..455dbd89 100644 --- a/src/util/scheduling.jl +++ b/src/util/scheduling.jl @@ -20,7 +20,7 @@ export TaskData, 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 @@ -110,6 +110,20 @@ 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) @@ -443,6 +457,22 @@ function wait(scheduler::SimulationScheduler, awaitable_task_data::AwaitableTask 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 diff --git a/src/util/topology.jl b/src/util/topology.jl index 8af00321..bde88494 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -2,7 +2,7 @@ export complete_topology, star_topology, cycle_topology, graph_topology, per_nod 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 + topology_connectors, topology_connection_types, NORMAL, INACTIVE, BROKEN, UNKNOWN, EXT_CONNECTION, State using MetaGraphsNext using Graphs 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/test/agent_tests.jl b/test/agent_tests.jl index ffd0d6d5..986182d2 100644 --- a/test/agent_tests.jl +++ b/test/agent_tests.jl @@ -365,4 +365,83 @@ end 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 \ No newline at end of file diff --git a/test/system_programming_tests.jl b/test/system_programming_tests.jl index b2c8bf58..58646d8d 100644 --- a/test/system_programming_tests.jl +++ b/test/system_programming_tests.jl @@ -6,6 +6,10 @@ using Dates got_it::Bool = false end +@role struct SystenProgrammingRole + got_it::Bool = false +end + @agent struct SystemInitAgent end struct MessageSystemProgramming end @@ -29,4 +33,66 @@ struct MessageSystemProgramming end 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/visualization_tests.jl b/test/visualization_tests.jl index 6e41ede7..28c0a8a3 100644 --- a/test/visualization_tests.jl +++ b/test/visualization_tests.jl @@ -2,6 +2,8 @@ using Mango using Test using Graphs using Dates +using Makie +using GraphMakie using CairoMakie @agent struct TopologyPlotAgent From 6be6fd75f21530b662e9b406c91d7d50b040d828 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 1 Jul 2025 22:22:29 +0200 Subject: [PATCH 45/54] Adding aid to agent description. Adding more tests for topolgoy features. --- README.md | 4 +- docs/src/getting_started.md | 2 +- docs/src/simulation.md | 2 +- .../src/communication.jl | 4 +- paper/paper.md | 2 +- src/agent/core.jl | 23 ++++-- src/container/core.jl | 2 +- src/simulation/container.jl | 2 +- src/simulation/world.jl | 73 +++++++++++++----- src/util/topology.jl | 77 +++++++++++-------- test/agent_tests.jl | 16 ++-- test/container_tests.jl | 8 +- test/topology_tests.jl | 22 ++++++ test/world_tests.jl | 46 +++++------ 14 files changed, 178 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 7516f79d..1bbcb8c7 100644 --- a/README.md +++ b/README.md @@ -88,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!" ) @@ -133,7 +133,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/getting_started.md b/docs/src/getting_started.md index 520ea884..dca8d9fe 100644 --- a/docs/src/getting_started.md +++ b/docs/src/getting_started.md @@ -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/simulation.md b/docs/src/simulation.md index fa00b27a..8706761b 100644 --- a/docs/src/simulation.md +++ b/docs/src/simulation.md @@ -28,7 +28,7 @@ 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/ext/MangoGraphVisualization/src/communication.jl b/ext/MangoGraphVisualization/src/communication.jl index b9564ac0..fcd00ce0 100644 --- a/ext/MangoGraphVisualization/src/communication.jl +++ b/ext/MangoGraphVisualization/src/communication.jl @@ -74,8 +74,8 @@ function Mango.plot_multi_agent_topology(topologies::Vector{Topology}; write_to: offset_i = i for j in 0:(length(topologies)-1) offset_j = j - aid_f = "$(connector.aid)-$offset_i" - aid_s = "$(other_connector.aid)-$offset_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 diff --git a/paper/paper.md b/paper/paper.md index a3ffd8dc..96ccd365 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -138,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/agent/core.jl b/src/agent/core.jl index 84fecb1f..beccaaaa 100644 --- a/src/agent/core.jl +++ b/src/agent/core.jl @@ -29,7 +29,9 @@ export @agent, color, category, update_description, - AgentDescription + AgentDescription, + uid, + uuid4 using UUIDs @@ -66,9 +68,11 @@ struct ForwardingRule end mutable struct AgentDescription + aid::Union{Nothing,String} name::String category::Symbol color::Symbol + uid::UUID end struct SystemHandler @@ -87,11 +91,10 @@ AGENT_BASELINE_FIELDS::Vector = [ :(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("", :agent, :gray)), + :(description::AgentDescription = AgentDescription(nothing, "", :agent, :gray, uuid4())), :(services::Dict{DataType,Any} = Dict{DataType,Any}()) ] @@ -354,14 +357,14 @@ function on_ready(agent::Agent) # do nothing by default end -function aid(agent::Agent) - return agent.aid -end - function description(agent::Agent) return agent.description end +function aid(agent::Agent) + return description(agent).aid +end + function name(agent::Agent) return description(agent).name end @@ -374,6 +377,10 @@ 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 @@ -604,7 +611,7 @@ function send_messages( agent.context.container, content, agent_address, - agent.aid; + aid(agent); kwargs..., )) end diff --git a/src/container/core.jl b/src/container/core.jl index 8d308f4b..0383c70a 100644 --- a/src/container/core.jl +++ b/src/container/core.jl @@ -104,7 +104,7 @@ function register( actual_aid = suggested_aid end container.agents[actual_aid] = agent - agent.aid = actual_aid + description(agent).aid = actual_aid agent.context = AgentContext(container, env) container.agent_counter += 1 diff --git a/src/simulation/container.jl b/src/simulation/container.jl index 9232d961..f4413d59 100644 --- a/src/simulation/container.jl +++ b/src/simulation/container.jl @@ -38,7 +38,7 @@ function register( actual_aid = suggested_aid end container.agents[actual_aid] = agent - agent.aid = actual_aid + description(agent).aid = actual_aid agent.context = AgentContext(container, container.env) container.agent_counter += 1 diff --git a/src/simulation/world.jl b/src/simulation/world.jl index ee1af6b4..ddb7ecd4 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -298,7 +298,7 @@ end function do_recordings(world::World) for collector in world.data_collectors - collector() + collector(world) end end @@ -312,6 +312,11 @@ function step_all_entities(world::World, time_step_s::Real) 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} @@ -337,22 +342,34 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT; max_adv 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(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 + 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 - world.container.step_size_s = time_step_s - step_all_entities(world, time_step_s) + 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, @@ -384,14 +401,23 @@ function step_simulation(world::World, step_size_s::Real=DISCRETE_EVENT; max_adv @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 - world.clock.simulation_time = add_seconds(time(world), time_step_s) - world.container.step_size_s = 0 + @debug "New time" time(world) - @debug "New time" time(world) + do_recordings(world) + end - do_recordings(world) + 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 @@ -406,6 +432,9 @@ This function will step the world until the clock has advanced to the initial_ti 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 = [] @@ -421,6 +450,8 @@ function discrete_step_until(world::World, max_advance_time_s::Real) 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 @@ -481,7 +512,7 @@ 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) - push!(world.data_collectors, () -> collector(world, data_collection(world, key))) + push!(world.data_collectors, (world) -> collector(world, data_collection(world, key))) end """ @@ -495,9 +526,9 @@ The data can be plotted using plot_agents. function collect_agent_data(collector::Function, world::World, key::String; dedicated_plots::Bool=false) dac = data_agent_collection(world, key, dedicated_plots=dedicated_plots) for agent in values(agents(world)) - push!(world.data_collectors, () -> collector(world, agent, dac)) + push!(world.data_collectors, (world) -> collector(world, agent, dac)) end - push!(world.data_collectors, () -> push!(dac.time, seconds_elapsed(clock(world)))) + push!(world.data_collectors, (world) -> push!(dac.time, seconds_elapsed(clock(world)))) end """ diff --git a/src/util/topology.jl b/src/util/topology.jl index bde88494..aca0efeb 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -2,7 +2,7 @@ export complete_topology, star_topology, cycle_topology, graph_topology, per_nod 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_connectors, topology_connection_types, NORMAL, INACTIVE, BROKEN, UNKNOWN, EXT_CONNECTION, State, topology_service using MetaGraphsNext using Graphs @@ -13,10 +13,15 @@ import Graphs.add_edge! agents::Vector{Agent} = Vector() end +struct TopologyNeighbor + address::AgentAddress + description::AgentDescription +end + @kwdef struct Topology tid::Symbol graph::MetaGraph - connectors::Vector{Tuple{Symbol,AgentAddress}} = Vector() # connection type to connector + connectors::Vector{Tuple{Symbol,TopologyNeighbor}} = Vector() # connection type to connector connections::Vector{Tuple{Symbol,Topology}} = Vector() # tid to connection type end @@ -29,8 +34,8 @@ end end @kwdef mutable struct TopologyService - tid_to_state_to_neighbors::Dict{Symbol,Dict{State,Vector{AgentAddress}}} = Dict() # tid to (edge state to agents) - tid_to_connectors::Dict{Symbol,Vector{Tuple{Symbol, AgentAddress}}} = Dict() # tid to (connection type to connected agents) + 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() end @@ -42,17 +47,17 @@ function service_node_id(service::TopologyService, tid::Symbol=:default) return service.tid_to_node_id[tid] end -function neighbors(service::TopologyService, tid::Symbol=:default, state::State=NORMAL; include_connectors::Vector{Symbol}=Vector{Symbol}()) +function neighbors(service::TopologyService, tid::Symbol=:default, state::State=NORMAL; include_connectors::Vector{Symbol}=Vector{Symbol}(), match_func::Function=(desc)->true) if haskey(service.tid_to_state_to_neighbors, tid) - return vcat(get(service.tid_to_state_to_neighbors[tid], state, Vector()), - [t[2] for t in service.tid_to_connectors[tid] if t[1] in include_connectors]) + return vcat([n.address for n in get(service.tid_to_state_to_neighbors[tid], state, Vector()) if 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 +end -function connectors(service::TopologyService, tid::Symbol=:default; include_connectors::Vector{Symbol}=Vector{Symbol}()) +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] for t in service.tid_to_connectors[tid] if t[1] in include_connectors || length(include_connectors) == 0] + 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 @@ -161,7 +166,7 @@ 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, address(a))) + push!(topology.connectors, (connector_type, TopologyNeighbor(address(a), description(a)))) end end @@ -205,13 +210,13 @@ 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, addr) in topology.connectors - if type == c_type && address(agent) == addr + 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_addr) in other_topo.connectors - if type == other_c_type - push!(connectors_for_agent, (type, other_addr)) + 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 @@ -220,24 +225,24 @@ function _build_connectors_list_for(topology, agent) return connectors_for_agent end -function _build_neighborhoods_and_inject(topology::Topology) +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{AgentAddress}} = Dict{State,Vector{AgentAddress}}() + 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, [address(agent) for agent in n_node.agents]) + append!(neighbor_addresses, [TopologyNeighbor(address(agent), description(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) - neighbor_addresses = get!(state_to_same, NORMAL, Vector()) - push!(neighbor_addresses, address(other_agent)) + neighbors = get!(state_to_same, NORMAL, Vector()) + push!(neighbors, TopologyNeighbor(address(other_agent), description(agent))) end end topology_service = service_of_type(agent, TopologyService, TopologyService()) @@ -246,16 +251,20 @@ function _build_neighborhoods_and_inject(topology::Topology) # look for marks and transfer to topology for type in topology_service.marked_connector_for - if !((type, address(agent)) in topology.connectors) - push!(topology.connectors, (type, address(agent))) + 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 """ @@ -403,12 +412,16 @@ end 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, include_connectors::Vector{Symbol}=Vector{Symbol}())::Vector{AgentAddress} - return neighbors(service_of_type(agent, TopologyService, TopologyService()), tid, state, include_connectors=include_connectors) +function topology_neighbors(agent::Agent; tid::Symbol=:default, state::State=NORMAL, include_connectors::Vector{Symbol}=Vector{Symbol}(), match_func::Function=(desc)->true)::Vector{AgentAddress} + return neighbors(service_of_type(agent, TopologyService, TopologyService()), tid, state, include_connectors=include_connectors, match_func=match_func) +end + +function topology_neighbors(role::Role; tid::Symbol=:default, state::State=NORMAL, 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, include_connectors=include_connectors, match_func=match_func) end -function topology_neighbors(role::Role; tid::Symbol=:default, state::State=NORMAL, include_connectors::Vector{Symbol}=Vector{Symbol}())::Vector{AgentAddress} - return neighbors(service_of_type(role.context.agent, TopologyService, TopologyService()), tid, state, include_connectors=include_connectors) +function topology_service(role::Role) + return service_of_type(role.context.agent, TopologyService, TopologyService()) end """ @@ -430,12 +443,12 @@ end 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}())::Vector{AgentAddress} - return connectors(service_of_type(agent, TopologyService, TopologyService()), tid, include_connectors=include_connectors) +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}())::Vector{AgentAddress} - return connectors(service_of_type(role.context.agent, TopologyService, TopologyService()), tid, include_connectors=include_connectors) +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 diff --git a/test/agent_tests.jl b/test/agent_tests.jl index 986182d2..4c043a85 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 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/topology_tests.jl b/test/topology_tests.jl index 2ae6401b..3fa3a106 100644 --- a/test/topology_tests.jl +++ b/test/topology_tests.jl @@ -239,4 +239,26 @@ end @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() + 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()] + + @warn marked_A + + 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 end \ No newline at end of file diff --git a/test/world_tests.jl b/test/world_tests.jl index d882486e..c13892cf 100644 --- a/test/world_tests.jl +++ b/test/world_tests.jl @@ -25,7 +25,7 @@ end register(world, agent1) register(world, agent2) - send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid), test=2) + send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=aid(agent1)), test=2) stepping_result = step_simulation(world, 1) @@ -66,8 +66,8 @@ end register(world, agent1) register(world, agent2) - send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + 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) @@ -87,8 +87,8 @@ end register(world, agent1) register(world, agent2) - send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + 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) @@ -109,16 +109,16 @@ end register(world, agent1) register(world, agent2) - send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + 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=agent1.aid)) - send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + 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) @@ -142,8 +142,8 @@ end 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=agent1.aid)) - send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + 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) @@ -183,8 +183,8 @@ end schedule(agent1, InstantTaskData()) do agent1.scheduled_counter += 100 end - send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + 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) @@ -216,8 +216,8 @@ end schedule(agent1, InstantTaskData()) do agent1.scheduled_counter += 100 end - send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + 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) @@ -249,8 +249,8 @@ end schedule(agent1, InstantTaskData()) do agent1.scheduled_counter += 100 end - send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + 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) @@ -285,8 +285,8 @@ end schedule(agent1, PeriodicTaskData(0.1)) do agent1.scheduled_counter += 1 end - send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid)) - send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + 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) @@ -332,8 +332,8 @@ end schedule(agent1, PeriodicTaskData(0.1)) do agent1.scheduled_counter += 1 end - send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid), agent2.aid) - send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + 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) @@ -367,8 +367,8 @@ end schedule(agent1, InstantTaskData()) do agent1.scheduled_counter += 1 end - send_message(world.container, "Hello Friends, this is RSc!", AgentAddress(aid=agent1.aid), agent2.aid) - send_message(world.container, "Hello Friends, this is RSd!", AgentAddress(aid=agent2.aid)) + 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) From a53a14cc05f30332c4433019b1e963bf4f9fbcd2 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Fri, 12 Sep 2025 17:57:32 +0200 Subject: [PATCH 46/54] Adding the possibility to capture data without plotting it in the overview plot. --- ext/MangoPlotVisualization/src/observation.jl | 4 +-- src/simulation/world.jl | 26 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/ext/MangoPlotVisualization/src/observation.jl b/ext/MangoPlotVisualization/src/observation.jl index 01f026a9..bb9bfb84 100644 --- a/ext/MangoPlotVisualization/src/observation.jl +++ b/ext/MangoPlotVisualization/src/observation.jl @@ -67,8 +67,8 @@ function Mango.plot_recordings(world::World; colormap=:viridis) row_length = 3 - dc = world.data_collections - dac = world.data_agent_collections + 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 = ( diff --git a/src/simulation/world.jl b/src/simulation/world.jl index ddb7ecd4..8deb0019 100644 --- a/src/simulation/world.jl +++ b/src/simulation/world.jl @@ -71,6 +71,7 @@ A WorldRecording is a container to record data in the world. timeseries::Vector{Any} = Vector() time::Vector{Real} = Vector() data::Any = nothing + no_plot = false end """ @@ -81,6 +82,7 @@ An AgentsRecording is a container to record data of the agents. time::Vector{Real} = Vector() data::Any = nothing dedicated_plots = false + no_plot = false end struct MessageTransaction @@ -492,8 +494,8 @@ end Return the data collection with the `key` from the world. """ -function data_collection(world::World, key::String) - return get!(world.data_collections, key, WorldRecording()) +function data_collection(world::World, key::String; no_plot::Bool=false) + return get!(world.data_collections, key, WorldRecording(no_plot=no_plot)) end """ @@ -501,8 +503,8 @@ end Return the data collection with the `key` from the world. """ -function data_agent_collection(world::World, key::String; dedicated_plots::Bool=false) - return get!(world.data_agent_collections, key, AgentsRecording(dedicated_plots=dedicated_plots)) +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 """ @@ -511,8 +513,8 @@ end 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) - push!(world.data_collectors, (world) -> collector(world, data_collection(world, 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 """ @@ -523,8 +525,8 @@ 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) - dac = data_agent_collection(world, key, dedicated_plots=dedicated_plots) +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 @@ -538,8 +540,8 @@ Record the world using the `world_recorder` function and store it in the data co The data can be plotted using plot_world. """ -function record_world!(world_recorder::Function, world::World, key::String) - collect_data(world, key) do w, dc +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 @@ -551,8 +553,8 @@ 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) - collect_agent_data(world, key, dedicated_plots=dedicated_plots) do w, a, dc +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 From ab301e3cca12a6ffe677df66432256fae0b4f9a7 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 30 Sep 2025 01:17:41 +0200 Subject: [PATCH 47/54] Add topology characteristic. --- Project.toml | 20 ++++++----- src/simulation/communication.jl | 14 ++++---- src/util/topology.jl | 64 +++++++++++++++++++++++++++------ test/topology_tests.jl | 28 +++++++++++++-- 4 files changed, 97 insertions(+), 29 deletions(-) diff --git a/Project.toml b/Project.toml index da3fc84f..8181f489 100644 --- a/Project.toml +++ b/Project.toml @@ -11,6 +11,7 @@ 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" @@ -22,12 +23,21 @@ Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" 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" +Documenter = "1.14.1" GraphMakie = "0.5.13" Graphs = "~1.10" JSON = "~0.21" @@ -39,19 +49,11 @@ OrderedCollections = "~1.6" Parameters = "~0.12" julia = "^1.9" -[weakdeps] -Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" -GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" - -[extensions] -MangoPlotVisualization = ["Makie"] -MangoGraphVisualization = ["Makie", "GraphMakie"] - [extras] CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test", "Documenter", "CairoMakie", "Makie", "GraphMakie"] diff --git a/src/simulation/communication.jl b/src/simulation/communication.jl index 0d90f947..bb976703 100644 --- a/src/simulation/communication.jl +++ b/src/simulation/communication.jl @@ -108,9 +108,9 @@ end function create_distribution_based_com_sim(aid_graph::MetaGraph, agents::Vector{Agent}; - default_delay_per_edge::Real=1, - base_delay_per_message::Real=20, - max_edge_delay::Real=100, + 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 @@ -118,10 +118,10 @@ function create_distribution_based_com_sim(aid_graph::MetaGraph, for edge in edges(aid_graph) from = src(edge) to = dst(edge) - distmatrix[from, to] = default_delay_per_edge - distmatrix[to, from] = default_delay_per_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) + 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) @@ -136,7 +136,7 @@ function create_distribution_based_com_sim(aid_graph::MetaGraph, if distance == typemax(Int) distance = 100 end - specific_distr = distribution_provider(base_delay_per_message + distance) + specific_distr = distribution_provider(base_delay_per_message_ms + distance) provider_com.delay_s_directed_edge_dict[(aid(agent), label_other)] = () -> 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 diff --git a/src/util/topology.jl b/src/util/topology.jl index aca0efeb..85f5d4b5 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -2,7 +2,8 @@ export complete_topology, star_topology, cycle_topology, graph_topology, per_nod 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 + topology_connectors, topology_connection_types, NORMAL, INACTIVE, BROKEN, UNKNOWN, EXT_CONNECTION, State, topology_service, + set_characteristic!, topology_characteristic using MetaGraphsNext using Graphs @@ -11,11 +12,21 @@ 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 + +function set_characteristic!(node::Node, agent::Agent, characteristic::Symbol) + node.characteristics[agent] = characteristic 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 @@ -25,6 +36,11 @@ end 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) @@ -38,6 +54,7 @@ end 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) @@ -47,9 +64,15 @@ function service_node_id(service::TopologyService, tid::Symbol=:default) return service.tid_to_node_id[tid] end -function neighbors(service::TopologyService, tid::Symbol=:default, state::State=NORMAL; include_connectors::Vector{Symbol}=Vector{Symbol}(), match_func::Function=(desc)->true) +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 match_func(n)], + 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")) @@ -69,6 +92,13 @@ function connection_types(service::TopologyService, tid::Symbol=:default) 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)] @@ -155,7 +185,7 @@ 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]) + topology.graph[vid] = Node(id=vid, agents=[a for a in agents]) return vid end @@ -225,6 +255,10 @@ function _build_connectors_list_for(topology, agent) 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) @@ -234,7 +268,7 @@ function _build_neighborhoods_and_inject(topology::Topology; build_connected=tru 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)) for agent in n_node.agents]) + 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!) @@ -242,12 +276,13 @@ function _build_neighborhoods_and_inject(topology::Topology; build_connected=tru 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(agent))) + 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 @@ -412,12 +447,12 @@ end 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, include_connectors::Vector{Symbol}=Vector{Symbol}(), match_func::Function=(desc)->true)::Vector{AgentAddress} - return neighbors(service_of_type(agent, TopologyService, TopologyService()), tid, state, include_connectors=include_connectors, match_func=match_func) +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, 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, include_connectors=include_connectors, match_func=match_func) +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) @@ -466,6 +501,15 @@ function topology_connection_types(role::Role; tid::Symbol=:default)::Vector{Sym 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) diff --git a/test/topology_tests.jl b/test/topology_tests.jl index 3fa3a106..13461c4c 100644 --- a/test/topology_tests.jl +++ b/test/topology_tests.jl @@ -253,12 +253,34 @@ end agents_1 = [marked_A, TopologyAgent(), TopologyAgent()] agents_2 = [marked_B, TopologyAgent(), TopologyAgent()] - - @warn marked_A - + 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 +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 end \ No newline at end of file From c0ece1d4f3067cb277c7e25d1ca4c409b01e3151 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 30 Sep 2025 01:37:27 +0200 Subject: [PATCH 48/54] Relaxing svg test. --- test/visualization_tests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/visualization_tests.jl b/test/visualization_tests.jl index 28c0a8a3..4d1436a1 100644 --- a/test/visualization_tests.jl +++ b/test/visualization_tests.jl @@ -109,6 +109,6 @@ end connect_topologies!(topology, topology2) plot_multi_agent_topology([topology, topology2], write_to="test_topology_plot.svg") - @test stat("test_topology_plot.svg").size == 13348 + @test stat("test_topology_plot.svg").size > 10000 rm("test_topology_plot.svg") end \ No newline at end of file From d2309231315ad1f5f94746d22923436e6adecf1b Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 30 Sep 2025 01:40:36 +0200 Subject: [PATCH 49/54] Relaxing svg test. --- Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Project.toml b/Project.toml index 8181f489..78e42c40 100644 --- a/Project.toml +++ b/Project.toml @@ -37,7 +37,6 @@ Colors = "~0.12" ConcurrentCollections = "~0.1" ConcurrentUtilities = "~2.2" Distributions = "~0.25" -Documenter = "1.14.1" GraphMakie = "0.5.13" Graphs = "~1.10" JSON = "~0.21" From 1453e130fcd17d179fb7d4a95e420ac107a7ffb0 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 30 Sep 2025 13:54:35 +0200 Subject: [PATCH 50/54] Adding tests for description, topology and communication provider. --- src/simulation/communication.jl | 2 +- src/util/topology.jl | 4 ---- test/agent_tests.jl | 21 +++++++++++++++++++++ test/topology_tests.jl | 4 +++- test/world_tests.jl | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/simulation/communication.jl b/src/simulation/communication.jl index bb976703..e172b57a 100644 --- a/src/simulation/communication.jl +++ b/src/simulation/communication.jl @@ -137,7 +137,7 @@ function create_distribution_based_com_sim(aid_graph::MetaGraph, distance = 100 end specific_distr = distribution_provider(base_delay_per_message_ms + distance) - provider_com.delay_s_directed_edge_dict[(aid(agent), label_other)] = () -> abs(rand(specific_distr)) / 1000 + 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 diff --git a/src/util/topology.jl b/src/util/topology.jl index 85f5d4b5..0d6d9ff9 100644 --- a/src/util/topology.jl +++ b/src/util/topology.jl @@ -15,10 +15,6 @@ import Graphs.add_edge! characteristics::Dict{Agent,Symbol} = Dict() # special agents having specific roles, e.g. :lead for coalition leaders end -function set_characteristic!(node::Node, agent::Agent, characteristic::Symbol) - node.characteristics[agent] = characteristic -end - struct TopologyNeighbor address::AgentAddress description::AgentDescription diff --git a/test/agent_tests.jl b/test/agent_tests.jl index 4c043a85..d33a2e76 100644 --- a/test/agent_tests.jl +++ b/test/agent_tests.jl @@ -444,4 +444,25 @@ end 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 \ No newline at end of file diff --git a/test/topology_tests.jl b/test/topology_tests.jl index 13461c4c..eebebcb1 100644 --- a/test/topology_tests.jl +++ b/test/topology_tests.jl @@ -259,6 +259,7 @@ end @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 @@ -283,4 +284,5 @@ end @test length(topology_neighbors(agent2, has_characteristic=:lead)) == 1 @test length(topology_neighbors(agent2)) == 2 @test length(topology_neighbors(agent2, has_characteristic=[:lead])) == 1 -end \ No newline at end of file + @test topology_node_id(agent) == 1 +end diff --git a/test/world_tests.jl b/test/world_tests.jl index c13892cf..4dbf16d1 100644 --- a/test/world_tests.jl +++ b/test/world_tests.jl @@ -2,6 +2,7 @@ using Mango using Test using Logging using Dates +using Random import Mango.handle_message @@ -484,4 +485,36 @@ end 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 From 4ed8327808f45b98c3a95ded661c57de7fbd310c Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 30 Sep 2025 15:15:53 +0200 Subject: [PATCH 51/54] Adding Random as package. --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index 78e42c40..ecacf7e2 100644 --- a/Project.toml +++ b/Project.toml @@ -20,6 +20,7 @@ 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" From 264d757532671c7860c70d800432b206a5a29fc5 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 30 Sep 2025 16:57:46 +0200 Subject: [PATCH 52/54] Adding more tests for some basic apis. --- src/environment/api.jl | 2 +- test/agent_tests.jl | 17 +++++++++++++++++ test/environment_api_tests.jl | 8 ++++++++ test/topology_tests.jl | 21 +++++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/environment/api.jl b/src/environment/api.jl index 01d5955f..cbdb0dd3 100644 --- a/src/environment/api.jl +++ b/src/environment/api.jl @@ -1,4 +1,4 @@ -export Position, Space, WorldObserver, Behavior, Environment, install, dispatch_global_event, initialize, initialized, add_observer! +export Position, Space, WorldObserver, Behavior, Environment, NoEnv, install, dispatch_global_event, initialize, initialized, add_observer! abstract type Position end abstract type WorldObserver end diff --git a/test/agent_tests.jl b/test/agent_tests.jl index d33a2e76..fbbacb82 100644 --- a/test/agent_tests.jl +++ b/test/agent_tests.jl @@ -465,4 +465,21 @@ end @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/environment_api_tests.jl b/test/environment_api_tests.jl index 33a8b63d..d2c94f00 100644 --- a/test/environment_api_tests.jl +++ b/test/environment_api_tests.jl @@ -49,4 +49,12 @@ struct TestSpace <: Space{TestPosition} end @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/topology_tests.jl b/test/topology_tests.jl index eebebcb1..e7356f59 100644 --- a/test/topology_tests.jl +++ b/test/topology_tests.jl @@ -262,6 +262,27 @@ end @test length(topology_connection_types(marked_A, tid=:A)) == 1 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 From 178bf4534c1baaa8f22645d570b777619563dc68 Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 30 Sep 2025 17:45:40 +0200 Subject: [PATCH 53/54] Adding topology and scheduling tests. --- src/util/scheduling.jl | 6 +++++- test/scheduler_tests.jl | 29 +++++++++++++++++++++++++++++ test/topology_tests.jl | 9 +++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/util/scheduling.jl b/src/util/scheduling.jl index 455dbd89..0d6521a4 100644 --- a/src/util/scheduling.jl +++ b/src/util/scheduling.jl @@ -11,11 +11,15 @@ export TaskData, stop_and_wait_for_all_tasks, schedule, Clock, + DateTimeClock, Scheduler, SimulationScheduler, AbstractScheduler, + AbstractClock, sleep_until, - seconds_elapsed + seconds_elapsed, + tasks, + clock using Dates using ConcurrentCollections diff --git a/test/scheduler_tests.jl b/test/scheduler_tests.jl index 104e3d05..575dcdc6 100644 --- a/test/scheduler_tests.jl +++ b/test/scheduler_tests.jl @@ -80,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/topology_tests.jl b/test/topology_tests.jl index e7356f59..d20024a0 100644 --- a/test/topology_tests.jl +++ b/test/topology_tests.jl @@ -247,6 +247,8 @@ end 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) @@ -260,6 +262,13 @@ end @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 From 83d91d95656b9b5d164813ab5caad1fdaa1e0b0a Mon Sep 17 00:00:00 2001 From: Rico Schrage Date: Tue, 30 Sep 2025 18:21:59 +0200 Subject: [PATCH 54/54] Docs QA, Version to 0.5.0 --- Project.toml | 2 +- docs/src/getting_started.md | 2 +- docs/src/topology.md | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index ecacf7e2..768f930b 100644 --- a/Project.toml +++ b/Project.toml @@ -2,7 +2,7 @@ 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" diff --git a/docs/src/getting_started.md b/docs/src/getting_started.md index dca8d9fe..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. diff --git a/docs/src/topology.md b/docs/src/topology.md index 781e4dd6..fa052f8d 100644 --- a/docs/src/topology.md +++ b/docs/src/topology.md @@ -80,12 +80,13 @@ 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 one complex topology. If you use more than only one topology, you can connect your topologies together to be linked on so-called `connectors`. +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, which 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 connector you can use [`mark_as_connector!`](@ref). +`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 -If you connect two topologies, say 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 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