MiniObserve.jl

Minimalist (and minimally intrusive) macro set for extracting information from complex objects
Author mhinsch
Popularity
0 Stars
Updated Last
1 Year Ago
Started In
June 2022

MiniObserve.jl

CI

Minimalist (and minimally intrusive) macro set for extracting information from complex objects.

Given a complex object MiniObserve lets you generate functions to extract and print information from that object by means of a simple declarative interface. It can for example be used to extract information from a simulation model at each time step and write that information to a file.

Using MiniObserve has several advantages over hand-written analysis code:

  • a concise declarative interface that puts all information in one place
  • output variables are declared once and output is automatically formatted correctly => adding and removing output variables becomes much less error-prone
  • a lot of tedious, repetitive and thus brittle code can be avoided

Usage

Most of the heavy lifting is done by the @observe macro:

@observe(statstype, model [, user_arg1...], declarations)

It will generate a custom data type to hold the desired information and an overload of the observe function that - given a model object - will calculate the information and return a data object.

Example

As a simple example, let us assume we have the following @observe declaration:

@observe Data model user1 user2 begin
	@record "time"      model.time
	@record "N"     Int length(model.population)

	@for ind in model.population begin
		@stat("capital", MaxMinAcc{Float64}, MeanVarAcc{FloatT}) <| ind.capital
		@stat("n_alone", CountAcc)           <| has_neighbours(ind)
	end

	@record u1			user1
	@record u2			user1 * user2
end

Then a type Data will be generated that provides (at least) the following members:

struct Data
	time :: Float64
	N :: Int
	capital :: @NamedTuple{max :: Float64, min :: Float64, mean :: Float64, var :: Float64}
	n_alone :: @NamedTuple{N :: Int}
	u1 :: Float64
	u2 :: Float64
end

Now we can call observe to obtain a data object filled with the corresponding values:

m = Model()
data = observe(Data, m, 1, 2)

And use print_header and log_results (both exported from MiniObserve) to print the content of that data object to a CSV file or a stream:

print_header(stdout, Data)
log_results(stdout, data)

Additional parameters

Note that @observe can take arbitrarily many parameters. Of these only the first two (the name of the data type and a model) and the last (the declaration itself) are required. All parameters between the second and the last are passed through unchanged and can for example be used to print additional information that is not part of the model object.

In fact there is nothing preventing a user from using @observe on several complex objects at the same time by for example passing two different model objects to observe.

Statistics

An important part of MiniObserve is the ability to analyse collections of items by funneling them through "accumulator" objects. This is particularly important for models that operate on populations of objects, such as agent-based or individual-based models.

In the example above this is used in the expression

	@for ind in model.population begin
		@stat("capital", MaxMinAcc{Float64}, MeanVarAcc{FloatT}) <| ind.capital
		@stat("n_alone", CountAcc)           <| has_neighbours(ind)
	end

The code generated by the macro iterates through model.population and adds some properties of each element to several accumulators that calculate maximum, minimum, variance and mean or count the number of true predicate values, respectively.

A few simple accumulators are provided in StatsAccumulator.

Used By Packages

No packages found.