API Documentation

Agent types

AlgebraicAgents.AbstractAlgebraicAgentType
AbstractAlgebraicAgent

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.

source

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!Method

Step an agent forward (call only if its projected time is equal to the least projected time, among all agents in the hierarchy).

source

Optional methods

Other optional methods include

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.setparameters!Function
setparameters!(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)))
source

Observables

AlgebraicAgents.getobservableFunction
getobservable(agent, args...)

Get agent's observable.

Examples

getobservable(getagent(agent, "../model"), "observable_name")
getobservable(getagent(agent, "../model"), 1)
source

Solving & plotting

AlgebraicAgents.step!Function
step!(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.

source
AlgebraicAgents.simulateFunction
simulate(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)
source

Paths

Implements path-like structure of agents.

AlgebraicAgents.getagentFunction

Retrieve an agent at path, relatively to agent.

source
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")
source
getagent(agent::AbstractAlgebraicAgent, path::AbstractString)

Get an agent given its relative path.

Examples

getagent(a, "../agent")
source
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/")
source
AlgebraicAgents.by_nameFunction
by_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.

source
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.

source

Opera, a dynamic structure to facilitate complex interactions

AlgebraicAgents.OperaType
Opera(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.

See poke and @call.

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
source
AlgebraicAgents.pokeFunction
poke(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
source
AlgebraicAgents.@callMacro
@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)`
source
AlgebraicAgents.add_instantious!Function
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.

Examples

add_instantious!(agent, () -> wake_up(agent))
source
AlgebraicAgents.@futureMacro
@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).

See also @future and Opera.

Examples

alice = MyAgentType("alice")
interact = agent -> wake_up!(agent)
@future alice 5.0 interact(alice) "alice_schedule" # stop at `t=5`
source
AlgebraicAgents.add_future!Function
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.

Examples

alice = MyAgentType("alice")
interact = agent -> wake_up!(agent)
add_future!(alice, 5.0, () -> interact(alice), "alice_schedule")
source
AlgebraicAgents.@controlMacro
@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"
source
AlgebraicAgents.add_control!Function
add_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.

See also @control and Opera.

Examples

system = MyAgentType("system")
control = agent -> agent.temp > 100 && cool!(agent)
add_control!(system, () -> control(system), "temperature control")
source

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");
source
⊕(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.

source
⊕(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.

source
⊕(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.

source
⊕(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.

source
AlgebraicAgents.@sumMacro
@sum models...

Perform an algebraic sum of algebraic models (flatten arguments to ⊕).

Examples

@sum m1 m2 m3 m4 # == ⊕(m1, m2, m3, m4)
source

Entangle and disentangle agents hierarchies.

AlgebraicAgents.disentangle!Function
disentangle!(agent)

Detach an agent from its parent. Optionally set remove_relpathrefs=false keyword to skip removing the relative pathrefs.

Examples

disentangle!(agent)
source

Agent type constructors

Supports convenient agent subtyping.

AlgebraicAgents.@aagentMacro
@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)
source

To provide custom specialization of @aagent convenience macros, see AlgebraicAgents.define_agent.

Walks

Walk agents' hierarchy.

AlgebraicAgents.prewalk_retFunction

Applies 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.

source
AlgebraicAgents.postwalk_retFunction

Applies 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.

source

Utility functions

Wrap a dynamical system, extract agent wrap

AlgebraicAgents.wrap_systemFunction
wrap_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))
source
AlgebraicAgents.extract_agentFunction
extract_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
source

Flat representation

Default plots for custom agent types

AlgebraicAgents.@draw_dfMacro
@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
source

Wires and wiring diagrams

AlgebraicAgents.add_wire!Function
add_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")
source
AlgebraicAgents.delete_wires!Function
delete_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")
source
AlgebraicAgents.retrieve_input_varsFunction
retrieve_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")
source
AlgebraicAgents.wiring_diagramFunction
wiring_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)
source

Helper functions for Mermaid diagrams

AlgebraicAgents.typetree_mmdFunction
typetree_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), ""))
source
AlgebraicAgents.agent_hierarchy_mmdFunction
agent_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,""))
source

Queries

It is possible to run filter and transform queries on agent hierarchies.

Filter queries

AlgebraicAgents.FilterQueryType
FilterQuery(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.

See also @f_str, filter.

Examples

filter(agents, f"_.age > 21 && _.name ∈ ['a', 'b']")
agents |> @filter _.age > 21 && _.name ∈ ['a', 'b']
source
AlgebraicAgents.@f_strMacro
f"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']")
source
Base.filterMethod
filter(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
source

To provide custom filter query types, you need to implement AlgebraicAgents._filter low-level matching method.

Transform queries

AlgebraicAgents.TransformQueryType
TransformQuery(name, query)

Simple transform query; references agents via underscores _.

See also @transform.

Examples

agent |> @transform(name=_.name)
agent |> @transform(name=_.name, _.age)
source
AlgebraicAgents.@transformMacro
@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.

source
AlgebraicAgents.transformFunction
transform(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)
source