Skip to content

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

License

Notifications You must be signed in to change notification settings

mhinsch/MiniObserve.jl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MiniObserve.jl

CI

Minimalist (and minimally intrusive) macro set for extracting information from complex objects, e.g. simulations.

Given a number of complicated data structures MiniObserve lets you generate functions to extract and print information from these objects by means of a simple, near 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, arg1 [, arg2...], 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 
		@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)

User code

Fundamentally @observe's operation is very simple, which makes it very flexible, but also easy to break. Any code in the declaration block will be copied into the observe function verbatim. The only changes @observe applies are:

  • At the beginning of the function a number of local variables containing the single analysis results are created.
  • Every occurence of @record and @stat is replaced with the appropriate code to store the result or add it to an accumulator object (see below), respectively.
  • At the end of the function the constructor of the analysis data type is called, collating all results into one data structure.

@observe does not perform any further sanity checks on code outside of the "pseudo-macros", so it is the user's responsibility to make sure not to break anything (it is for example a very bad idea to add a return statement to the analysis code).

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

About

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

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages