diff --git a/Project.toml b/Project.toml index a76262e..db579aa 100644 --- a/Project.toml +++ b/Project.toml @@ -4,16 +4,15 @@ version = "0.3.11" [deps] Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" -DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Glob = "c27321d9-0574-5035-807b-f59d2c89b15c" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Requires = "ae029012-a4dd-5104-9daa-d747884805df" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] Crayons = "4.1" -DataStructures = "0.18" Glob = "1.3" MacroTools = "0.5" Requires = "1.3" diff --git a/docs/src/design.md b/docs/src/design.md index b8b9196..3dbb075 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -24,11 +24,49 @@ The inner area enclosed by a dashed border represents where program control is g ## Opera -The Opera system allows interactions between agents to be scheduled, which will be executed at the end of a time step, sorted by priority. By default, AlgebraicAgents.jl provides support for two types of interactions: +The Opera system allows interactions between agents to be scheduled. By default, AlgebraicAgents.jl provides support for three types of interactions: - * [`@schedule`](@ref) is used to schedule a "wake up" call to the agent, custom behavior can be implemented by defining [`AlgebraicAgents._interact!`](@ref) for subtypes of `AbstractAlgebraicAgent`. - * [`@schedule_call`](@ref) is used to schedule a callback function to the agent. + * **delayed interactions** + * **controls** + * **instantious interactions** + [`poke`](@ref) is used to schedule a "wake up" call to the agent, custom behavior can be implemented by defining [`AlgebraicAgents._interact!`](@ref) for subtypes of `AbstractAlgebraicAgent`. + * [`@call`](@ref) is used to schedule a callback function to the agent. -However the system can work with arbitrary types of interactions. To do so, simply define a new call type that is a subtype of `AbstractOperaCall`. The methods `execute_action!` and `opera_enqueue!` must be specialized for your new call type. After that, your new interaction type can be used just like any other! To see an example, please check out our tests. +However the system can work with arbitrary types of interactions. To do so, simply define a new call type that is a subtype of `AbstractOperaCall`. The methods `execute_action!` and `add_instantious!` must be specialized for your new call type. After that, your new interaction type can be used just like any other! To see an example, please check out our tests. -For more details, see the API documentation of [`Opera`](@ref) and our tests. \ No newline at end of file +For more details, see the API documentation of [`Opera`](@ref) and our tests. + +A dynamic structure that + - contains a **directory of algebraic agents** (dictionary of `uuid => agent` pairs); + - keeps track of, and executes, **futures (delayed interactions)**; + - keeps track of, and executes, **system controls**; + - keeps track of, and executes, **instantious interactions**; + +### Future Interactions + +You may schedule function calls, to be executed at predetermined points of time. +The action is specified as a tuple `(id, call, time)`, where `id` is an optional textual identifier of the action, `call` is a (parameterless) anonymous function, which will be called at given `time`. +Once the action is executed, the return value with corresponding action id and execution time is added to `futures_log` field of `Opera` instance. + +See [`add_future!`](@ref) and [`@future`](@ref). + +### Control Interactions + +You may schedule control function calls, to be executed at every step of the model. +The action is specified as a tuple `(id, call)`, where `id` is an optional textual identifier of the action, and `call` is a (parameterless) anonymous function. +Once the action is executed, the return value with corresponding action id and execution time is added to `controls_log` field of `Opera` instance. + +See [`add_control!`](@ref) and [`@control`](@ref). + +### Instantious Interactions + +You may schedule additional interactions which exist within a single step of the model; +such actions are modeled as named tuples `(id, priority=0., call)`. Here, `call` is a (parameterless) anonymous function. + +They exist within a single step of the model and are executed after the calls +to `_prestep!` and `_step!` finish, in the order of the assigned priorities. + +In particular, you may schedule interactions of two kinds: + + - `poke(agent, priority)`, which will translate into a call `() -> _interact!(agent)`, with the specified priority, + - `@call opera expresion priority`, which will translate into a call `() -> expression`, with the specified priority. \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md index 2c477c4..4ce0c27 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -107,9 +107,13 @@ by_name ```@docs Opera -AbstractOperaCall -AgentCall -opera_enqueue! +poke +@call +add_instantious! +@future +add_future! +@control +add_control! ``` ### Operations @@ -162,12 +166,10 @@ postwalk_ret @get_agent ``` -### Observable accessor, interaction schedulers +### Retrieving observables ```@docs @observables -@schedule -@schedule_call ``` ### Flat representation diff --git a/docs/src/sketches/sciml.md b/docs/src/sketches/sciml.md index d544442..a0c6d5d 100644 --- a/docs/src/sketches/sciml.md +++ b/docs/src/sketches/sciml.md @@ -49,10 +49,10 @@ function f_(u,p,t) # schedule interaction ## first, schedule a call to `_interact!(agent)` with priority 0 ## this is the default behavior - @schedule agent + poke(agent) ## alternatively, provide a function call f(args...) ## this will be expanded to a call f(agent, args...) - @schedule_call agent custom_function(t) + @call agent custom_function(agent, t) min(2., 1.01*u + o1 + o2 + o3) end diff --git a/src/AlgebraicAgents.jl b/src/AlgebraicAgents.jl index 5fa05f1..92bd846 100644 --- a/src/AlgebraicAgents.jl +++ b/src/AlgebraicAgents.jl @@ -4,9 +4,9 @@ using Requires using Glob using UUIDs -using DataStructures using MacroTools using Crayons +using Random: randstring # abstract algebraic agent types include("abstract.jl") @@ -21,8 +21,10 @@ export getagent, by_name, entangle!, disentangle! # and and which contains a directory of algebraic integrators include("opera.jl") export AbstractOperaCall, AgentCall, Opera -## enqueue an action -export opera_enqueue! +# Opera interface +export add_instantious!, poke, @call +export add_future!, @future +export add_control!, @control # utility functions include("utils.jl") @@ -31,7 +33,7 @@ export @wrap ## declare derived sequence export @derived ## convenient observable accessor, interaction schedulers -export @observables, @schedule, @schedule_call +export @observables ## flat representation of agent hierarchy export flatten ## instantiate an integration and add it to Julia's load path diff --git a/src/integrations/SciMLIntegration/core.jl b/src/integrations/SciMLIntegration/core.jl index 5c6d8b2..e62ef70 100644 --- a/src/integrations/SciMLIntegration/core.jl +++ b/src/integrations/SciMLIntegration/core.jl @@ -184,7 +184,10 @@ getobservable(::Val{DummyType}, args...) = 0 gettimeobservable(::Val{DummyType}, args...) = 0 getopera(::Val{DummyType}) = Val(DummyType) AgentCall(::Val{DummyType}, args...) = Val(DummyType) -opera_enqueue!(::Val{DummyType}, args...) = nothing +add_instantious!(::Val{DummyType}, args...) = nothing +get_count(::Val{DummyType}, args...) = "nothing" +add_future!(::Val{DummyType}, args...) = "nothing" +add_control!(::Val{DummyType}, args...) = "nothing" # custom pretty-printing function print_custom(io::IO, mime::MIME"text/plain", a::DiffEqAgent) diff --git a/src/interface.jl b/src/interface.jl index f928db0..3f3a4f5 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -195,7 +195,11 @@ function step!(a::AbstractAlgebraicAgent, t = projected_to(a); isroot = true) end @ret ret _projected_to(a) - isroot && opera_run!(getopera(a)) + if isroot + execute_instantious_interactions!(getopera(a), t) + @ret ret execute_futures!(getopera(a), t) + execute_controls!(getopera(a), t) + end ret end @@ -205,10 +209,16 @@ end Return `true` if all algebraic agent's time horizon was reached (or `nothing` in case of delegated evolution). Else return the minimum time up to which the evolution of an algebraic agent, and all its descendants, has been projected. """ -function projected_to(a::AbstractAlgebraicAgent) +function projected_to(a::AbstractAlgebraicAgent; isroot = true) ret = _projected_to(a) foreach(values(inners(a))) do a - @ret ret projected_to(a) + @ret ret projected_to(a; isroot = false) + end + + if isroot + foreach(getopera(a).futures) do i + @ret ret i.time + end end ret @@ -218,12 +228,14 @@ end function _projected_to(t::AbstractAlgebraicAgent) @error("type $(typeof(t)) doesn't implement `_projected_to`") end + _projected_to(::FreeAgent) = nothing "Step an agent forward (call only if its projected time is equal to the least projected time, among all agents in the hierarchy)." function _step!(a::AbstractAlgebraicAgent) @error "algebraic agent $(typeof(a)) doesn't implement `_step!`" end + _step!(::FreeAgent) = nothing "Pre-step to a step call (e.g., projecting algebraic agent's solution up to time `t`)." diff --git a/src/opera.jl b/src/opera.jl index ff3b83a..fa530a7 100644 --- a/src/opera.jl +++ b/src/opera.jl @@ -1,72 +1,326 @@ -# implements an interaction broker +# interaction broker -"Abstract opera interaction. See [`Opera`](@ref)." -abstract type AbstractOperaCall end - -"A scheduled call to an agent. See [`Opera`](@ref)." -struct AgentCall <: AbstractOperaCall - agent::AbstractAlgebraicAgent - call::Any - - AgentCall(agent::AbstractAlgebraicAgent, call = nothing) = new(agent, call) -end +## action types +const InstantiousInteraction = NamedTuple{(:id, :call, :priority), + <:Tuple{AbstractString, Function, Number}} +const InstantiousInteractionLog = NamedTuple{(:id, :time, :retval), + <:Tuple{AbstractString, Number, Any}} +const Future = NamedTuple{(:id, :call, :time), + <:Tuple{AbstractString, Function, Any}} +const FutureLog = NamedTuple{(:id, :time, :retval), + <:Tuple{AbstractString, Any, Any}} +const Control = NamedTuple{(:id, :call), <:Tuple{AbstractString, Function}} +const ControlLog = NamedTuple{(:id, :time, :retval), <:Tuple{AbstractString, Any, Any}} """ Opera(uuid2agent_pairs...) -A dynamic structure that stores a **priority queue of algebraic interactions** -and contains a **directory of algebraic agents** (dictionary of `uuid => agent` pairs). +A dynamic structure that + - contains a **directory of algebraic agents** (dictionary of `uuid => agent` pairs); + - keeps track of, and executes, **futures (delayed interactions)**; + - keeps track of, and executes, **system controls**; + - keeps track of, and executes, **instantious interactions**; -# Algebraic Interactions -It is possible to schedule additional interactions within the complex; -such actions are instances of `AbstractOperaCall`, -and they are modeled as tuples `(priority=0., call)`. +# Futures +You may schedule function calls, to be executed at predetermined points of time. +The action is specified as a tuple `(id, call, time)`, where `id` is an optional textual identifier of the action, `call` is a (parameterless) anonymous function, which will be called at given `time`. +Once the action is executed, the return value with corresponding action id and execution time is added to `futures_log` field of `Opera` instance. -At the end of the topmost call to `step!`, the actions will be executed one-by-one in order of the respective priorities. +See [`add_future!`](@ref) and [`@future`](@ref). -In particular, you may schedule interactions of two kinds: - - - `@schedule agent priority=0`, which will translate into a call `_interact!(agent)`, - - `@schedule_call agent f(args...) priority=0` or `@schedule_call agent x->ex priority=0`, -which will translate into a call `agent->f(agent, args...)` or `(x->ex)(agent)`, respectively. +# Control Interactions +You may schedule control function calls, to be executed at every step of the model. +The action is specified as a tuple `(id, call)`, where `id` is an optional textual identifier of the action, and `call` is a (parameterless) anonymous function. +Once the action is executed, the return value with corresponding action id and execution time is added to `controls_log` field of `Opera` instance. + +See [`add_control!`](@ref) and [`@control`](@ref). -See [`@schedule`](@ref) and [`@schedule_call`](@ref). +# Instantious Interactions +You may schedule additional interactions which exist within a single step of the model; +such actions are modeled as named tuples `(id, priority=0., call)`. Here, `call` is a (parameterless) anonymous function. They exist within a single step of the model and are executed after the calls -to `_prestep!` and `_step!` finish. +to `_prestep!` and `_step!` finish, in the order of the assigned priorities. -See [`opera_enqueue!`](@ref). +In particular, you may schedule interactions of two kinds: + + - `poke(agent, priority)`, which will translate into a call `() -> _interact!(agent)`, with the specified priority, + - `@call opera expresion priority`, which will translate into a call `() -> expression`, with the specified priority. + +See [`poke`](@ref) and [`@call`](@ref). """ mutable struct Opera - calls::PriorityQueue{AbstractOperaCall, Float64} + # dictionary of `uuid => agent` pairs directory::Dict{UUID, AbstractAlgebraicAgent} + # intantious interactions + instantious_interactions::Vector{InstantiousInteraction} + instantious_interactions_log::Vector{InstantiousInteractionLog} + n_instantious_interactions::Ref{UInt} + # futures + futures::Vector{Future} + futures_log::Vector{FutureLog} + n_futures::Ref{UInt} + # controls + controls::Vector{Control} + controls_log::Vector{ControlLog} + n_controls::Ref{UInt} function Opera(uuid2agent_pairs...) - new(PriorityQueue{AbstractOperaCall, Float64}(Base.Order.Reverse), - Dict{UUID, AbstractAlgebraicAgent}(uuid2agent_pairs...)) + new(Dict{UUID, AbstractAlgebraicAgent}(uuid2agent_pairs...), + Vector{InstantiousInteraction}(undef, 0), + Vector{InstantiousInteractionLog}(undef, 0), + 0, + Vector{Future}(undef, 0), + Vector{FutureLog}(undef, 0), + 0, + Vector{Control}(undef, 0), + Vector{ControlLog}(undef, 0), + 0) end end -"Schedule an algebraic interaction." -function opera_enqueue!(::Opera, ::AbstractOperaCall, ::Float64) end +# increase the count the number of anonymous interactions of the given count, +# and return the count +function get_count(opera::Opera, type::Symbol) + (getproperty(opera, type)[] += 1) |> string +end -function opera_enqueue!(opera::Opera, call::AgentCall, priority::Float64 = 0.0) - !haskey(opera.calls, call) && enqueue!(opera.calls, call => priority) +# dispatch on the interaction call, and execute it +function call(opera::Opera, call::Function) + if hasmethod(call, Tuple{}) + call() + elseif hasmethod(call, Tuple{Opera}) + call(opera) + elseif length(opera.directory) > 1 && + hasmethod(call, Tuple{typeof(topmost(first(opera.directory).value))}) + call(topmost(first(opera.directory).value)) + else + @error """interaction $call must have one of the following forms: + - be parameterless, + - be a function of `Opera` instance, + - be a function of the topmost agent in the hierarchy. + """ + end +end + +""" + add_instantious!(opera, call, priority=0[, id]) + add_instantious!(agent, call, priority=0[, id]) +Schedule a `call` to be executed in the current time step. + +Interactions are implemented within an instance `Opera`, sorted by their priorities. + +See also [`Opera`](@ref). + +# Examples +```julia +add_instantious!(agent, () -> wake_up(agent)) +``` +""" +function add_instantious!(opera::Opera, call, priority::Number = 0.0, + id = "instantious_" * + get_count(opera, :n_instantious_interactions)) + add_instantious!(opera, (; id, call, priority)) end -"Execute an algebraic interaction." -function execute_action!(::Opera, ::AbstractOperaCall) end +function add_instantious!(agent::AbstractAlgebraicAgent, args...) + add_instantious!(getopera(agent), args...) +end -function execute_action!(::Opera, call::AgentCall) - if isnothing(call.call) - _interact!(call.agent) +function add_instantious!(opera::Opera, action::InstantiousInteraction) + # sorted insert + insert_at = searchsortedfirst(opera.instantious_interactions, action; + by = x -> x.priority) + insert!(opera.instantious_interactions, insert_at, action) +end + +# Execute instantious interactions +function execute_instantious_interactions!(opera::Opera, time) + while !isempty(opera.instantious_interactions) + action = pop!(opera.instantious_interactions) + log_record = (; id = action.id, time, retval = call(opera, action.call)) + + push!(opera.instantious_interactions_log, log_record) + end +end + +""" + poke(agent, priority=0[, id]) +Poke an agent in the current time step. Translates to a call `() -> _interact(agent)`, see [`call`](@ref). + +Interactions are implemented within an instance `Opera`, sorted by their priorities. + +See also [`Opera`](@ref). + +# Examples +```julia +poke(agent) +poke(agent, 1.) # with priority equal to 1 +``` +""" +function poke(agent, priority::Number = 0.0, + id = "instantious_" * get_count(getopera(agent), :n_instantious_interactions)) + add_instantious!(getopera(agent), + (; id, call = () -> _interact!(agent), + priority = Float64(priority))) +end + +""" + @call agent call [priority[, id]] + @call opera call [priority[, id]] +Schedule an interaction (call), which will be executed in the current time step. +Here, `call` will translate into a function `() -> call`. + +Interactions are implemented within an instance `Opera`, sorted by their priorities. + +See also [`Opera`](@ref). + +# Examples +```julia +bob_agent = only(getagent(agent, r"bob")) +@call agent wake_up(bob_agent) # translates into `() -> wake_up(bob_agent)` +``` +""" +macro call(opera, call, priority::Number = 0.0, id = nothing) + quote + opera = $(esc(opera)) isa Opera ? $(esc(opera)) : getopera($(esc(opera))) + id = if isnothing($(esc(id))) + "instantious_" * get_count(opera, :n_instantious_interactions) + else + $(esc(id)) + end + + add_instantious!(opera, + (; id, call = () -> $(esc(call)), + priority = Float64($(esc(priority))))) + end +end + +""" + add_future!(opera, time, call[, id]) + add_future!(agent, time, call[, id]) +Schedule a (delayed) execution of `call` at `time`. Optionally, provide a textual identifier `id` of the action. + +Here, `call` has to follow either of the following forms: + - be parameterless, + - be a function of `Opera` instance, + - be a function of the topmost agent in the hierarchy. +This follows the dynamic dispatch. + +See also [`Opera`](@ref). +""" +function add_future! end + +function add_future!(opera::Opera, time, call, + id = "future_" * get_count(opera, :n_futures)) + new_action = (; id, call, time) + + # sorted insert + insert_at = searchsortedfirst(opera.futures, new_action, by = x -> x.time) + insert!(opera.futures, insert_at, new_action) +end + +function add_future!(agent::AbstractAlgebraicAgent, args...) + add_future!(getopera(agent), args...) +end + +""" + @future opera time call [id] + @future agent time call [id] +Schedule a (delayed) execution of `call` at `time`. Optionally, provide a textual identifier `id` of the action. + +`call` is an expression, which will be wrapped into a function `() -> call`. + +See also [`@future`](@ref) and [`Opera`](@ref). +""" +macro future(opera, time, call, id = nothing) + quote + opera = $(esc(opera)) isa Opera ? $(esc(opera)) : getopera($(esc(opera))) + id = if isnothing($(esc(id))) + "future_" * get_count(opera, :n_futures) + else + $(esc(id)) + end + + add_future!(opera, $(esc(time)), () -> $(esc(call)), + id) + end +end + +# execute futures (delayed interactions) +function execute_futures!(opera::Opera, time) + while !isempty(opera.futures) + action = first(opera.futures) + if action.time <= time + # execute, log + log_record = (; id = action.id, time, retval = call(opera, action.call)) + push!(opera.futures_log, log_record) + + # delete action + popfirst!(opera.futures) + else + break + end + end + + # least time among scheduled actions + if isempty(opera.futures) + nothing else - call.call(call.agent) + first(opera.futures).time + end +end + +""" + add_control!(opera, call[, id]) + add_future!(agent, call[, id]) +Add a control to the system. Optionally, provide a textual identifier `id` of the action. + +Here, `call` has to follow either of the following forms: + - be parameterless, + - be a function of `Opera` instance, + - be a function of the topmost agent in the hierarchy. +This follows the dynamic dispatch. + +See also [`@control`](@ref) and [`Opera`](@ref). +""" +function add_control! end + +function add_control!(opera::Opera, call, id = "control_" * get_count(opera, :n_controls)) + new_action = (; id, call) + + push!(opera.controls, new_action) +end + +function add_control!(agent::AbstractAlgebraicAgent, args...) + add_control!(getopera(agent), args...) +end + +""" + @control opera call [id] + @control agent call [id] +Add a control to the system. Optionally, provide a textual identifier `id` of the action. + +`call` is an expression, which will be wrapped into an anonymous, parameterless function `() -> call`. + +See also [`Opera`](@ref). +""" +macro control(opera, call, id = nothing) + quote + opera = $(esc(opera)) isa Opera ? $(esc(opera)) : getopera($(esc(opera))) + id = if isnothing($(esc(id))) + id = "control_" * get_count(opera, :n_controls) + else + $(esc(id)) + end + + add_control!($(esc(opera)), () -> $(esc(call)), id) end end -"Execute scheduled algebraic interactions." -function opera_run!(opera::Opera) - while !isempty(opera.calls) - execute_action!(opera, dequeue!(opera.calls)) +# execute system controls +function execute_controls!(opera::Opera, time) + foreach(opera.controls) do action + log_record = (; id = action.id, time, retval = call(opera, action.call)) + push!(opera.controls_log, log_record) end end diff --git a/src/paths.jl b/src/paths.jl index 8e7e578..fc48962 100644 --- a/src/paths.jl +++ b/src/paths.jl @@ -136,13 +136,18 @@ function disentangle!(agent::AbstractAlgebraicAgent; remove_relpathrefs = true) isnothing(getparent(agent)) && return agent opera_inners = Opera() + # copy interactions + foreach(setdiff(fieldnames(Opera), (:directory,))) do f + setproperty!(opera_inners, f, getproperty(getopera(agent), f) |> deepcopy) + end + inners_uuid = UUID[] prewalk(agent) do a push!(inners_uuid, getuuid(a)) push!(opera_inners.directory, getuuid(a) => a) end - # for each algebraic agent, rm relpath reference to an agent + # sync operas for hierarchy under agent prewalk(agent) do agent_ sync_opera!(agent_, opera_inners) end diff --git a/src/utils.jl b/src/utils.jl index 2a031fd..2093a25 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -21,7 +21,7 @@ macro ret(old, ret) end end -iscontinuable(ret) = !isnothing(ret) && (ret !== true) +iscontinuable(ret) = !isnothing(ret) && !isa(ret, Bool) "Turns aargs into a uuid-indexed dict." function yield_aargs(a::AbstractAlgebraicAgent, aargs...) @@ -36,52 +36,6 @@ function yield_aargs(a::AbstractAlgebraicAgent, aargs...) naargs end -" - @schedule agent priority=0 -Schedule an interaction. Interactions are implemented within an instance `Opera`, sorted by their priorities. -Internally, reduces to `_interact!(agent)`. - -See also [`Opera`](@ref). - -# Examples -```julia -@schedule agent 1. -``` -" -macro schedule(agent, priority = 0) - quote - opera_enqueue!(getopera($(esc(agent))), AgentCall($(esc(agent))), - Float64($(esc(priority)))) - end -end - -" - @schedule agent call priority=0 -Schedule an interaction (call). Interactions are implemented within an instance `Opera`, sorted by their priorities. -Internally, the `call=f(args...)` expression will be transformed to an anonymous function `agent -> f(agent, args...)`. - -See also [`Opera`](@ref). - -# Examples -```julia -@schedule agent f(t) -``` -" -macro schedule_call(agent, call, priority = 0) - call = if call isa Expr && Meta.isexpr(call, :call) - sym = gensym() - insert!(call.args, 2, sym) - :($sym -> $(call)) - else - call - end - - quote - opera_enqueue!(getopera($(esc(agent))), AgentCall($(esc(agent)), $(esc(call))), - Float64($(esc(priority)))) - end -end - """ @observables agent path:obs path:(obs1, obs2) path:[obs1, obs2] Retrieve (a vector of) observables relative to `agent`. diff --git a/test/Project.toml b/test/Project.toml index 869fb65..2420440 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -4,7 +4,6 @@ AlgebraicDynamics = "5fd6ff03-a254-427e-8840-ba658f502e32" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Catlab = "134e5e36-593f-5add-ad60-77f754baafbe" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" diff --git a/test/opera.jl b/test/opera.jl index d7f3a08..59abc4a 100644 --- a/test/opera.jl +++ b/test/opera.jl @@ -1,5 +1,4 @@ using Test, AlgebraicAgents -using DataStructures: enqueue! @testset "opera interaction with two agents on different time steps" begin @aagent struct MyAgent{T <: Real} @@ -18,9 +17,9 @@ using DataStructures: enqueue! function AlgebraicAgents._step!(a::MyAgent{T}) where {T} if a.name == "alice" - @schedule only(getagent(a, r"bob")) 0 + poke(only(getagent(a, r"bob")), 0.0) else - @schedule only(getagent(a, r"alice")) 0 + poke(only(getagent(a, r"alice"))) end a.counter1 += 1 @@ -45,6 +44,24 @@ using DataStructures: enqueue! @test bob.counter2 == 9 @test bob.counter1_t == collect(0:1.5:7.5) @test bob.counter2_t == [1.5, 1.5, 3, 4.5, 4.5, 6, 7.5, 7.5, 9] + + opera = getopera(joint_system) + @test length(opera.instantious_interactions_log) == 15 + @test opera.instantious_interactions_log[1].id == "instantious_1" + @test opera.instantious_interactions_log[2].id == "instantious_2" + + # check if operas get out of sync after disentangling the agents + disentangle!(alice) + add_instantious!(alice, () -> nothing) + add_instantious!(alice, () -> nothing, 0) + add_instantious!(bob, () -> nothing, 0) + + @test length(getopera(alice).instantious_interactions) == 2 + @test length(getopera(bob).instantious_interactions) == 1 + + @test getopera(alice).instantious_interactions[1].id == "instantious_17" + @test getopera(alice).instantious_interactions[2].id == "instantious_16" + @test getopera(bob).instantious_interactions[1].id == "instantious_16" end @testset "opera agent call with two agents on different time steps" begin @@ -67,9 +84,9 @@ end function AlgebraicAgents._step!(a::MyAgent1{T}) where {T} tnow = a.time if a.name == "alice" - @schedule_call only(getagent(a, r"bob")) (a)->poke_other(a, tnow) + @call a poke_other(only(getagent(a, r"bob")), tnow) else - @schedule_call only(getagent(a, r"alice")) (a)->poke_other(a, tnow) + @call a poke_other(only(getagent(a, r"alice")), tnow) end a.counter1 += 1 @@ -98,61 +115,76 @@ end @test bob.counter2_tt == alice.counter1_t end -@testset "test custom AbstractOperaCall" begin - - # subtype of AbstractOperaCall which Opera can work with - struct TwoAgentCall{A <: AbstractAlgebraicAgent, B <: AbstractAlgebraicAgent, - C <: Function} <: AbstractOperaCall - agentA::A - agentB::B - call::C - end - - @aagent struct MyAgent2{T <: Real, M <: AbstractString} +@testset "futures" begin + @aagent struct MyAgent2{T <: Real} time::T Δt::T - myinfo::M + + max_time::T end - function interact_together(a, b) - tmp = a.myinfo - a.myinfo = b.myinfo - b.myinfo = tmp + # future: call + interact = agent -> agent + + function AlgebraicAgents._step!(a::MyAgent2{T}) where {T} + a.time += a.Δt end - # Opera interface functions which need specialization - function AlgebraicAgents.execute_action!(::Opera, call::TwoAgentCall) - call.call(call.agentA, call.agentB) + AlgebraicAgents._projected_to(a::MyAgent2) = a.time >= a.max_time ? true : a.time + + alice = MyAgent2{Float64}("alice", 0.0, 1.0, 10.0) + bob = MyAgent2{Float64}("bob", 0.0, 1.5, 15.0) + + joint_system = ⊕(alice, bob, name = "joint") + + @future alice 5.0 interact(alice) "alice_schedule" + @future bob 20.0 interact(bob) + + simulate(joint_system, 100.0) + + opera = getopera(joint_system) + + @test isempty(opera.futures) + @test length(opera.futures_log) == 2 + @test opera.futures_log[1].retval == alice + @test opera.futures_log[2].retval == bob +end + +@testset "control interactions" begin + @aagent struct MyAgent3{T <: Real} + time::T + Δt::T + + max_time::T end - function AlgebraicAgents.opera_enqueue!(opera::Opera, call::TwoAgentCall, - priority::Float64 = 0.0) - !haskey(opera.calls, call) && enqueue!(opera.calls, call => priority) + # control + control = function (model::AbstractAlgebraicAgent) + projected_to(model) end - # general interface functions for MyAgent2 types - function AlgebraicAgents._step!(a::MyAgent2{T, M}) where {T, M} - if a.name == "alice" - opera_enqueue!(getopera(a), - TwoAgentCall(a, only(getagent(a, r"bob")), interact_together)) - end + control_alice = agent -> getname(agent) + function AlgebraicAgents._step!(a::MyAgent3{T}) where {T} a.time += a.Δt end - AlgebraicAgents._projected_to(a::MyAgent2) = a.time + AlgebraicAgents._projected_to(a::MyAgent3) = a.time >= a.max_time ? true : a.time - # simulate - alice = MyAgent2{Float64, String}("alice", 0.0, 1.0, "alice's info") - bob = MyAgent2{Float64, String}("bob", 0.0, 1.0, "bob's info") + alice = MyAgent3{Float64}("alice", 0.0, 1.0, 10.0) + bob = MyAgent3{Float64}("bob", 0.0, 1.5, 15.0) joint_system = ⊕(alice, bob, name = "joint") - @test alice.myinfo == "alice's info" - @test bob.myinfo == "bob's info" + @control alice control_alice(alice) "control_alice" + @control joint_system control(joint_system) + + simulate(joint_system, 100.0) - simulate(joint_system, 1.0) + opera = getopera(joint_system) - @test alice.myinfo == "bob's info" - @test bob.myinfo == "alice's info" + @test length(opera.controls) == 2 + @test length(opera.controls_log) == 32 + @test opera.controls_log[1].retval == "alice" + @test opera.controls_log[2].retval == 1.0 end diff --git a/test/sciml/sciml_test.jl b/test/sciml/sciml_test.jl index 47a3eaf..7e2930e 100644 --- a/test/sciml/sciml_test.jl +++ b/test/sciml/sciml_test.jl @@ -38,10 +38,10 @@ function f_(u, p, t) # schedule interaction ## first, schedule a call to `_interact!(agent)` with priority 0 ## this is the default behavior - @schedule agent + poke(agent) ## alternatively, provide a function call f(args...) ## this will be expanded to a call f(agent, args...) - @schedule_call agent custom_function(t) + @call agent custom_function(agent, t) min(2.0, 1.01 * u + o1 + o2 + o3) end diff --git a/tutorials/sciml_tutorial/tutorial.jl b/tutorials/sciml_tutorial/tutorial.jl index 1ddc7dd..aa23c83 100644 --- a/tutorials/sciml_tutorial/tutorial.jl +++ b/tutorials/sciml_tutorial/tutorial.jl @@ -38,10 +38,10 @@ function f_(u, p, t) # schedule interaction ## first, schedule a call to `_interact!(agent)` with priority 0 ## this is the default behavior - @schedule agent + poke(agent) ## alternatively, provide a function call f(args...) ## this will be expanded to a call f(agent, args...) - @schedule_call agent custom_function(t) + @call agent custom_function(agent, t) min(2.0, 1.01 * u + o1 + o2 + o3) end