System

In Cropbox, a system is a unit of model component that contains a collection of variables. The framework guarantees that the most current state of a variable is accessible by another variable that depends on said variable, provided that they are within the same system. To ensure a correct propagation of variable states, the system must have a linear order of computation that satisfies all the requirements for dependency imposed by variable declarations. Any inconsistency caused by a cyclic dependency between variables stops code generation and results in an error. This is intentional, as we want to avoid such logical errors from going through unnoticed.

Once a system is defined, its structure is fixed and variables cannot be added or removed. The variables themselves, however, can still be updated throughout time steps. Variables declared in another system can be also accessed if the entire system holding dependent variables has already been updated. This is done by declaring an external system as a member of another system.

Creating a System

A system in Cropbox is created through the Cropbox-specific macro, @system.

@system name[{patches..}][(mixins..)] [<: type] [decl] -> Type{<:System}

@system declares a new system called name, with new variables declared in decl block using a custom syntax. mixins allow specifications of existing systems to be used for the new system. patches may provide type substitution and/or constant definition needed for advanced use.

Example

Here is an example of what a simple system may look like.

@system begin
    i => 1 ~ preserve
    a => 0.1 ~ preserve(parameter)
    r(a, x) => a*x ~ track
    x(r) ~ accumulate(init = i)
end

In this system, we declared four variables.

  • i: variable containing initial value of x which never changes (preserved)
  • a: variable containing constant parameter of exponential growth
  • r: rate variable which needs to be calculated or tracked every time step
  • x: state variable which accumulates by rate r over time with initial value i
Note

We can use the Julia macro @macroexpand to see the expression generated by the @system macro.

Mixin

A mixin is a system that is included as a part another system. While each system implements its own set of variables, these variables can be linked with variables from other systems through the use of mixins. Controller is a mixin required to instantiate a system.

Example

Here is an example where the system S3 is declared with systems S1 and S2 as mixins.

@system S1 begin
    a => 1 ~ preserve(parameter)
    b(a) => 2a ~ track
end

@system S2 begin
    a => 2 ~ preserve(parameter)
    b(a, c) => a*c ~ track
    c => 1 ~ preserve
end

@system S3(S1, S2, Controller) begin
    d(a) => 3a ~ preserve
end

instance(S3)
S3
context=<Context>
config=<Config>
a=2.0
b=2.0
c=1.0
d=6.0
Note

The order of mixins when creating a system is significant. When two variables from two different mixins share a name, the variable from the latter mixin in the system declaration will take priority over the first.

Context

Whenever a system is constructed in Cropbox, an internal variable named context referencing to an instance of a Context system is included by default. The purpose of the Context system is to manage the time and configuration of a system.

This is what the Context system looks like:

@system Context begin
    context       ~ ::Nothing
    config        ~ ::Config(override)
    clock(config) ~ ::Clock
end

The variables config and clock, referencing to the systems Config and Clock respectively, are necessary for system instantiation and thus included in every new system by default.

Clock

Within the Context system, there is a clock variable referring to the Clock system. The Clock system is responsible for keeping track of time-related variables, namely init, step, time, and tick.

This is what the Clock system looks like:

abstract type Clock <: System end
timeunit(::Type{<:Clock}) = u"hr"
@system Clock{timeunit = timeunit(Clock)} begin
    context ~ ::Nothing
    config ~ ::Config(override)
    init => 0 ~ preserve(unit=timeunit, parameter)
    step => 1 ~ preserve(unit=timeunit, parameter)
    time => nothing ~ advance(init=init, step=step, unit=timeunit)
    tick => nothing ~ advance::int
end

time is an advance variable which is essentially an accumulate variable tailored for keeping time of simulation. By default, time starts at hour 0 and increases by 1-hour intervals. tick is another time variable that is responsible for keeping track of the number of updates performed. As a result, context.clock.time and context.clock.tick are often used as the index for the x-axis of plots and visualizations. The step variable is determines the time-step intervals of simulation.

Config

Unlike the Clock system, the Config referred to by the config variable in Context is not a system. Config is a configuration object structured as a nested dictionary or hash table to store user-defined parameter values as a triplet of system - variable - value. When a configuration object containing specified parameter values are provided to a instantiation of a system, the corresponding variables in the system with the tag parameter will have the configuration values plugged in. Read more about configurations here.

Controller

An instance of context and configuration provided to an instance of a new system is usually sourced by a parent system that holds a variable referring to that system. However, because there is no parent system for the instantiation of the first system, context and configuration need to be supplied elsewhere. Controller is a pre-built system of Cropbox made to handle such issues by creating an instance of Context by itself.

This is what the Controller system looks like:

@system Controller begin
    config ~ ::Config(override)
    context(config) ~ ::Context(context)
end

The Config object referred to by the config variable is overridden by a keyword argument (config) of the system constructor instance() and functions such as simulate() and visualize(). Therefore at least one (and usually only one) system is designated to possess Controller as one of its mixins. In order to run an instance or a simulation of a system, Controller must be included as a mixin. Unlike the system Context, Controller must be explicitly declared as a mixin when declaring a system.

Tip

When you create a system that you want to instantiate, make sure to have Controller as a mixin. You can also make a system instantiable by making a new system with the original system and Controller as mixins.

Calendar

Calendar is a system similar to the Clock system. Calendar provides time and step variables, but in the type of ZonedDateTime from TimeZones.jl. Much like Clock, Calendar is a pre-built Cropbox system, but Calendar is not included by default as a variable reference like context for the system Context. Also, unlike Clock, Calendar is not embedded in Context.

This is what the Calendar system looks like:

@system Calendar begin
    init ~ preserve::datetime(extern, parameter)
    last => nothing ~ preserve::datetime(extern, parameter, optional)
    time(t0=init, t=context.clock.time) => t0 + convert(Cropbox.Dates.Second, t) ~ track::datetime
    date(time) => Cropbox.Dates.Date(time) ~ track::date
    step(context.clock.step) ~ preserve(u"hr")
    stop(time, last) => begin
        isnothing(last) ? false : (time >= last)
    end ~ flag
    count(init, last, step) => begin
        if isnothing(last)
            nothing
        else
            # number of update!() required to reach `last` time
            (last - init) / step
        end
    end ~ preserve::int(round, optional)
end