API Documentation
Agent types
AlgebraicAgents.AbstractAlgebraicAgent
— TypeAbstractAlgebraicAgent
Abstract supertype of all algebraic agents. This is a dynamic structure which parametrizes dynamics of the agent, stores additional data required for the numerical simulation, and optionally logs its state at selected timesteps.
AlgebraicAgents.FreeAgent
— TypeA container of agents. Doesn't implement a standalone evolutionary rule; delegates evolution to internal agents.
AlgebraicAgents.FreeAgent
— MethodFreeAgent(name, agents=[])
Initialize an agent. Optionally provide contained agents at the time of instantiation. See also entangle!
and disentangle!
.
Examples
FreeAgent("agent", [agent1, agent2])
Implementing custom types
To implement a custom agent type, you may want to use the convenience macro @aagent
which supplies type fields expected (not required, though) by the interface.
Required methods
AlgebraicAgents._step!
— MethodStep an agent forward (call only if its projected time is equal to the least projected time, among all agents in the hierarchy).
AlgebraicAgents._projected_to
— MethodReturn time to which agent's evolution was projected.
Optional methods
AlgebraicAgents._getparameters
— Method_getparameters(agent)
Retrieve parameter space of an agent.
AlgebraicAgents._setparameters!
— Method_setparameters!
Mutate agent's parameter space.
Examples
_setparameters!(agent, Dict(:α=>1))
_setparameters!(agent, [1., 2.])
AlgebraicAgents._draw
— MethodReturn plot of an agent's state. Defaults to nothing
.
AlgebraicAgents._reinit!
— MethodReinitialize the state of an agent.
AlgebraicAgents._interact!
— MethodWake up an agent. See Opera
.
AlgebraicAgents._prestep!
— MethodPre-step to a step call (e.g., projecting agent's solution up to time t
).
Other optional methods include
getobservable(::AbstractAlgebraicAgent, ::Any)
observables(::AbstractAlgebraicAgent)
wrap_system
extract_agent
Loading third-party package integrations
So far, integrations of DifferentialEquations.jl, Agents.jl, and AlgebraicDynamics.jl are provided.
Loading of the integrations is facilitated by Requires.jl; the integration will automatically be included once the respective third-party package is loaded.
For example,
using AlgebraicAgents
@isdefined DiffEqAgent
true
using AlgebraicAgents, DifferentialEquations
@isdefined DiffEqAgent
wrap_system("my_model", ODEProblem((u, p, t) -> 1.01*u, [1/2], (0., 10.)))
agent my_model with uuid 8b681910 of type DiffEqAgent
custom properties:
integrator:
t: 0.0
u: 1-element Vector{Float64}:
0.5
For plotting, you will want to load Plots
as well. Nevertheless, function draw
will inform you when necessary.
Common interface
Agent properties accessors
AlgebraicAgents.getname
— Functiongetname(agent)
Get agent's name.
AlgebraicAgents.getuuid
— Functiongetuuid(agent)
Get agent's uuid.
AlgebraicAgents.getparent
— Functiongetparent(agent)
Get agent's parent.
AlgebraicAgents.inners
— Functioninners(agent)
Get dictionary of agent's inner agents. Follows name => agent
format.
AlgebraicAgents.getopera
— FunctionGet agent's Opera
.
AlgebraicAgents.getdirectory
— FunctionGet agent's directory. See also Opera
.
AlgebraicAgents.getparameters
— Functiongetparameters(agent)
Retrieve agents' (incl. inner agents, if applicable) parameter space.
AlgebraicAgents.setparameters!
— Functionsetparameters!(agent, parameters)
Assign agent's parameters. Parameters are accepted in the form of a dictionary containing path => params
pairs.
Examples
setparameters!(agent, Dict("agent1/agent2" => Dict(:α=>1)))
Observables
AlgebraicAgents.observables
— Functionobservables(agent)
Return a list of observables (explicitly) exported by an agent. Use getobservable
to retrieve the observable's value.
AlgebraicAgents.getobservable
— Functiongetobservable(agent, args...)
Get agent's observable.
Examples
getobservable(getagent(agent, "../model"), "observable_name")
getobservable(getagent(agent, "../model"), 1)
AlgebraicAgents.gettimeobservable
— FunctionGet agent's observable at a given time.
Solving & plotting
AlgebraicAgents.step!
— Functionstep!(agent, t=projected_to(agent))
Performs a single evolutionary step of the hierarchy. To avoid frontrunning, solutions will be projected only up to time t
. This is a two-phase step; the corresponding stepping functions are _prestep!
and step!
.
More particular behavior can be implemented using Opera
protocol.
For custom agents' types, it suffices to implement _step!
.
Return values
Return true
if all internal agent's time horizon was reached. Else return the minimum time up to which the agent's solution was projected.
AlgebraicAgents.simulate
— Functionsimulate(agent::AbstractAlgebraicAgent, max_t=Inf)::AbstractAlgebraicAgent
Solves an (initialized) problem. Runs a loop until all the agents return true
(reached simulation horizon) or nothing
(delegated evolution), or until the simulation horizon reaches max_t
. Avoids front-running.
Examples
sol = simulate(model)
AlgebraicAgents.draw
— FunctionPlot an agent's state. For internal implementation, see _draw
.
Paths
Implements path-like structure of agents.
AlgebraicAgents.@glob_str
— MacroReturns a glob string to enable wildcard matching of agents paths.
AlgebraicAgents.@uuid_str
— MacroReturns UUID object given a uuid string.
AlgebraicAgents.getagent
— FunctionRetrieve an agent at path
, relatively to agent
.
getagent(a::AbstractAlgebraicAgent, uuid::UUID)
Get an agent given its uuid.
Examples
getagent(a, UUID("2a634aad-0fbe-4a91-a605-bfbef4d57f95"))
getagent(a, uuid"2a634aad-0fbe-4a91-a605-bfbef4d57f95")
getagent(agent::AbstractAlgebraicAgent, path::AbstractString)
Get an agent given its relative path.
Examples
getagent(a, "../agent")
getagent(agent::AbstractAlgebraicAgent, path::Union{Glob.FilenameMatch, Regex})
Get an agent given a regex or glob string.
Examples
getagent(a, r"agent.*")
getagent(a, glob"**/agent/")
AlgebraicAgents.by_name
— Functionby_name(agent, name::AbstractString; inners_only=false)
Return agents in the hierachy with the given name
. If inners_only==true
, consider descendants of agent
only.
by_name(agent, name::Union{Glob.FilenameMatch, Regex})
Return agents in the hierarchy whose names match the given wildcard. If inners_only==true
, consider descendants of agent
only.
Opera, a dynamic structure to facilitate complex interactions
AlgebraicAgents.Opera
— TypeOpera(uuid2agent_pairs...)
A dynamic structure that
- contains a directory of 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;
Futures
You may schedule function calls, to be executed at predetermined points of time. An action is modeled as a tuple (id, call, time)
, where id
is an optional textual identifier of the action and call
is a (parameterless) anonymous function, which will be called at the 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!
and @future
.
Example
alice = MyAgentType("alice")
interact = agent -> wake_up!(agent)
@future alice 5.0 interact(alice) "alice_schedule"
The solver will stop at t=5
and call the function () -> interact(alice)
(a closure is taken at the time when @future
is invoked). This interaction is identified as "alice_schedule"
.
Control Interactions
You may schedule control function calls, to be executed at every step of the model. An action is modeled 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!
and @control
.
Example
system = MyAgentType("system")
control = agent -> agent.temp > 100 && cool!(agent)
@control system control(system) "temperature control"
At each step, the solver will call the function () -> control(system)
(a closure is taken at the time when @future
is invoked).
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.
Examples
# `poke`
poke(agent, 1.) # call `_interact!(agent)`; this call is added to the instantious priority queue with priority 1
# `@call`
bob_agent = only(getagent(agent, r"bob"))
@call agent wake_up(bob_agent) # translates into `() -> wake_up(bob_agent)` with priority 0
AlgebraicAgents.poke
— Functionpoke(agent, priority=0[, id])
Poke an agent in the current time step. Translates to a call () -> _interact(agent)
, see @call
.
Interactions are implemented within an instance Opera
, sorted by their priorities.
See also Opera
.
Examples
poke(agent)
poke(agent, 1.) # with priority equal to 1
AlgebraicAgents.@call
— Macro@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
.
Examples
bob_agent = only(getagent(agent, r"bob"))
@call agent wake_up(bob_agent) # translates into `() -> wake_up(bob_agent)`
AlgebraicAgents.add_instantious!
— Functionadd_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
.
Examples
add_instantious!(agent, () -> wake_up(agent))
AlgebraicAgents.@future
— Macro@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
(taking closure at the time when @future
is invoked).
Examples
alice = MyAgentType("alice")
interact = agent -> wake_up!(agent)
@future alice 5.0 interact(alice) "alice_schedule" # stop at `t=5`
AlgebraicAgents.add_future!
— Functionadd_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
.
Examples
alice = MyAgentType("alice")
interact = agent -> wake_up!(agent)
add_future!(alice, 5.0, () -> interact(alice), "alice_schedule")
AlgebraicAgents.@control
— Macro@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
.
Examples
system = MyAgentType("system")
control = agent -> agent.temp > 100 && cool!(agent)
@control system control(system) "temperature control"
AlgebraicAgents.add_control!
— Functionadd_control!(opera, call[, id])
add_control!(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.
Examples
system = MyAgentType("system")
control = agent -> agent.temp > 100 && cool!(agent)
add_control!(system, () -> control(system), "temperature control")
Operations
Defines sums of agents.
AlgebraicAgents.:⊕
— Function⊕(models::Vararg{AbstractAlgebraicAgent, N}; name)
Algebraic sum of algebraic models. Optionally specify resulting model's name.
By default, outputs an instance of FreeAgent
.
Examples
⊕(m1, m2; name="diagram1") ⊕ ⊕(m3, m4; name="diagram2");
⊕(system1, system2; diagram=pattern, name)
⊕([system1, system2]; diagram=pattern, name)
⊕(Dict(:system1 => system1, :system2 => system2); diagram=pattern, name)
Apply oapply(diagram, systems...)
and wrap the result as a GraphicalAgent
.
⊕(system1, system2; diagram=pattern, name)
⊕([system1, system2]; diagram=pattern, name)
⊕(Dict(:system1 => system1, :system2 => system2); diagram=pattern, name)
Apply oapply(diagram, systems...)
and wrap the result as a GraphicalAgent
.
⊕(system1, system2; diagram=pattern, name)
⊕([system1, system2]; diagram=pattern, name)
⊕(Dict(:system1 => system1, :system2 => system2); diagram=pattern, name)
Apply oapply(diagram, systems...)
and wrap the result as a GraphicalAgent
.
⊕(system1, system2; diagram=pattern, name)
⊕([system1, system2]; diagram=pattern, name)
⊕(Dict(:system1 => system1, :system2 => system2); diagram=pattern, name)
Apply oapply(diagram, systems...)
and wrap the result as a GraphicalAgent
.
AlgebraicAgents.@sum
— Macro@sum models...
Perform an algebraic sum of algebraic models (flatten arguments to ⊕).
Examples
@sum m1 m2 m3 m4 # == ⊕(m1, m2, m3, m4)
Entangle and disentangle agents hierarchies.
AlgebraicAgents.entangle!
— Functionentangle!(parent, agent)
Push an agent to the hierachy.
Examples
entagle!(parent, ancestor)
AlgebraicAgents.disentangle!
— Functiondisentangle!(agent)
Detach an agent from its parent. Optionally set remove_relpathrefs=false
keyword to skip removing the relative pathrefs.
Examples
disentangle!(agent)
Agent type constructors
Supports convenient agent subtyping.
AlgebraicAgents.@aagent
— Macro@aagent [OptionalBasetype=FreeAgent] [OptionalSupertype=AbstractAlgebraicAgent] struct my_agent
extra_fields...
end
Define a custom agent type, and include fields expected by default interface methods (see FreeAgent
).
Fields are mutable by default, but can be declared immutable using const
keyword.
Provides a constructor which takes agent's name at the input, and populates the common fields.
Example
@aagent struct Molecule
age::Float64
birth_time::Float64
sales::Float64
end
Optional base type:
@aagent FreeAgent struct Molecule
age::Float64
birth_time::Float64
sales::Float64
end
Optional base type and a super type:
@aagent FreeAgent AbstractMolecule struct Molecule
age::Float64
birth_time::Float64
sales::Float64
end
Parametric types:
@aagent struct MyAgent{T <: Real, P <: Real}
field1::T
field2::P
end
MyAgent{Float64, Int}("myagent", 1, 2)
To provide custom specialization of @aagent
convenience macros, see AlgebraicAgents.define_agent
.
AlgebraicAgents.define_agent
— Functiondefine_agent(base_type, super_type, type, __module, constructor)
A function to define an agent type. See the definition of @aagent
.
Walks
Walk agents' hierarchy.
AlgebraicAgents.prewalk
— FunctionApplies f
to each agent. Applies f
to an agent before visiting its inners.
AlgebraicAgents.postwalk
— FunctionApplies f
to each agent. Applies f
to an agent after visiting its inners.
AlgebraicAgents.prewalk_ret
— FunctionApplies f
to each agent. Applies f
to an agent before visiting its inners. The results of each application of f
are appended to a vector and returned.
AlgebraicAgents.postwalk_ret
— FunctionApplies f
to each agent. Applies f
to an agent after visiting its inners. The results of each application of f
are appended to a vector and returned.
Utility functions
Wrap a dynamical system, extract agent wrap
AlgebraicAgents.wrap_system
— Functionwrap_system(name, system, args...; kwargs...)
Typically, the function will dispatch on the type of system
and initialise an algebraic agent which wraps the core dynamical system. This allows you to specify the core dynamics directly using a third-party package syntax and hide the internals on this package's side from the user.
For instance, you may define a method wrap_system(name, prob::DiffEqBase.DEProblem)
, which internally will invoke the constructor of DiffEqAgent
.
Examples
wrap_system("ode_agent", ODEProblem(f, u0, tspan))
wrap_system("abm_agent", ABM(agent, space; properties))
AlgebraicAgents.extract_agent
— Functionextract_agent
Extract an agent from as a property of the dynamical system (wrapped by the agent).
Examples
agent = extract_agent(params) # for SciML integration
agent = extract_agent(model, agent) # for ABM integration
Flat representation
AlgebraicAgents.flatten
— Functionflatten(root_agent)
Return flat representation of agents' hierarchy.
Default plots for custom agent types
AlgebraicAgents.@draw_df
— Macro@draw_df T field
A macro to define _draw(T)
such that it will plot a DataFrame stored under field
.
Requires DataFrames
and Plots
to be available.
Examples
@draw_df my_type log # will plot `log` property (a DataFrame) of `my_type`'s instance
Wires and wiring diagrams
AlgebraicAgents.get_wires_from
— Functionget_wires_from(a)
Get wires originating from an agent.
AlgebraicAgents.get_wires_to
— Functionget_wires_to(a)
Get wires going into an agent.
AlgebraicAgents.add_wire!
— Functionadd_wire!(a; from, to, from_var_name, to_var_name)
Add a wire connecting two agents.
Examples
add_wire!(joint_system; from=alice, to=bob, from_var_name="alice_x", to_var_name="bob_x")
AlgebraicAgents.delete_wires!
— Functiondelete_wires!(a; from, to, from_var_name, to_var_name)
Delete wires connecting two agents. Optionally, specify source and target variables.
Examples
delete_wires!(joint_system; from=alice, to=bob)
delete_wires!(joint_system; from=alice, to=bob, from_var_name="alice_x", to_var_name="bob_x")
AlgebraicAgents.retrieve_input_vars
— Functionretrieve_input_vars(a)
Return a dictionary with values along wires going into a
, specificed as target => value
pairs.
retrieve_input_vars(alice) # Dict("alice1_x" => "alice")
AlgebraicAgents.wiring_diagram
— Functionwiring_diagram(agent; parentship_edges=true, wires=true)
wiring_diagram(agents; parentship_edges=true, wires=true)
wiring_diagram(groups; group_labels=nothing, parentship_edges=true, wires=true)
Render a Graphviz graph of agents in an hierarchy. By default, the graph shows edges between parent and child agents, and annotated wires.
Also see agent_hierarchy_mmd
.
Examples
# Build a compound problem.
joint_system = ⊕(alice, bob, name = "joint system")
wiring_diagram(joint_system)
# Do not show edges between parents and children.
wiring_diagram(joint_system; parentship_edges=false)
# Only show listed agents.
wiring_diagram([alice, alice1, bob, bob1])
# Group agents into two clusters.
wiring_diagram([[alice, alice1], [bob, bob1]])
# Provide labels for clusters.
wiring_diagram([[alice, alice1], [bob, bob1]]; group_labels=["alice", "bob"], parentship_edges=false)
Helper functions for Mermaid diagrams
AlgebraicAgents.typetree_mmd
— Functiontypetree_mmd(T, TT; rem = false)
Return a Vector{String}
of the type hierarchy with type T
, in format suitable for making Mermaid class diagrams. For the root case (where T
is the top of the hierarchy), TT
may be set to nothing (default argument).
The keyword argument rem
can be set to true to strip the module prefix from typenames. This is useful for Mermaid diagrams, because the Mermaid classDiagram does not currently support "." characters in class names.
Examples
# the following may be pasted into the Mermaid live editor:
# https://mermaid.live/
print(join(typetree_mmd(Integer), ""))
AlgebraicAgents.agent_hierarchy_mmd
— Functionagent_hierarchy_mmd(a; use_uuid = 0)
This function can help display the agent hierarchy for concrete models. It assumes the user wants to pass the results into a Mermaid diagram for easier visualization of concrete model instantiations. The kwarg use_uuid
will append the last use_uuid
digits of each agent to their name following an underscore. This can be useful if it is not possible to distinguish unique agents purely by their name alone.
Examples
# the following may be pasted into the Mermaid live editor:
# https://mermaid.live/
@aagent FreeAgent struct AgentType1 end
base = FreeAgent("agent1")
entangle!(base, AgentType1("agent2"))
entangle!(base, AgentType1("agent3"))
# do not print UUIDs
hierarchy = agent_hierarchy_mmd(base)
print(join(hierarchy,""))
# print last 4 digits of UUIDs
hierarchy = agent_hierarchy_mmd(base, use_uuid = 4)
print(join(hierarchy,""))
Queries
It is possible to run filter and transform queries on agent hierarchies.
Filter queries
AlgebraicAgents.FilterQuery
— TypeFilterQuery(query)
Simple property query; references agents via underscores _
.
A query on an agent may result in an error; in that case, the agent will fail the filter condition by default.
Examples
filter(agents, f"_.age > 21 && _.name ∈ ['a', 'b']")
agents |> @filter _.age > 21 && _.name ∈ ['a', 'b']
AlgebraicAgents.@f_str
— Macrof"query"
Turn a query string into a query instance, see also FilterQuery
.
Supports string interpolations.
Examples
filter(agents, f"_.age > 1 && _.name ∈ ['a', 'b']")
i = 1; filter(agents, f"_.age > $i && _.name ∈ ['a', 'b']")
AlgebraicAgents.@filter
— Macro@filter query
Turn a filter query into a function of agents' hierarchy. Accepts expressions (corresponding to q-strings) and query string.
See also FilterQuery
.
Base.filter
— Methodfilter(agent::AbstractAlgebraicAgent, queries...)
filter(agents::Vector{<:AbstractAlgebraicAgent}, queries...)
Run filter query on agents in a hierarchy.
Examples
filter(agent, f"_.age > 21 && _.name ∈ ['a', 'b']") # filter query
To provide custom filter query types, you need to implement AlgebraicAgents._filter
low-level matching method.
AlgebraicAgents._filter
— Function_filter(agent, query)
Check if an agent satisfies filter condition.
Transform queries
AlgebraicAgents.TransformQuery
— TypeTransformQuery(name, query)
Simple transform query; references agents via underscores _
.
See also @transform
.
Examples
agent |> @transform(name=_.name)
agent |> @transform(name=_.name, _.age)
AlgebraicAgents.@transform
— Macro@transform queries...
Turn transform queries into an anonymous function of agents' hierarchy. See also TransformQuery
.
Accepts both anonymous queries (_.name
) and named queries (name=_.name
). By default, includes agent's uuid.
AlgebraicAgents.transform
— Functiontransform(agent::AbstractAlgebraicAgent, queries...)
tranform(agent::Vector{<:AbstractAlgebraicAgent}, queries...)
Run transform query on agents in a hierarchy.
A query on an agent may result in an error; in that case, the respective agent's output is omitted for the result.
See also @transform
.
Examples
agent |> @transform(name=_.name)
agent |> @transform(name=_.name, _.age)