Modeling and Visualizing Calendar Time

ABS provides a logical clock, a rational-valued global counter starting at 0 – see the Timed ABS chapter of the manual for details. This example shows how to use ABS to run simulations modeling real (calendar) time and visualize the resulting data in a timeline.

The code can be found at https://github.com/abstools/absexamples/tree/master/collaboratory/examples/time-and-date/

Modeling Time in ABS

The following short ABS model creates a list of values together with the time when the value was produced. The results are stored in a Calendar object that is accessible via the ABS Model API.

During the simulation, the method addValue is called with random values. The calendar object keeps track of the value and the time when it was added.

The calendar also makes two methods callable via the Model API: getValues returns a list of pairs of recorded time and value. The method getStartDate returns a string containing a date in ISO 8601 notation; this is the “real” start date that will be used in the simulation. The time values in the list returned by getValues will be interpreted as offset from that point in time.

module CalendarTime;

def String start_date() = "2020-04-01";

interface Calendar {
    [HTTPCallable] String getStartDate();
    [HTTPCallable] List<Pair<Rat, Int>> getValues();

    Unit addValue(Int value);
}

class Calendar implements Calendar {
    List<Pair<Rat, Int>> values = list[];

    String getStartDate() { return start_date(); }
    List<Pair<Rat, Int>> getValues() { return reverse(values); }

    Unit addValue(Int value) {
        values = Cons(Pair(timeValue(now()), value), values);
    }
}

{
    [HTTPName: "Calendar"] Calendar c = new Calendar();
    Int nRounds = 16;
    while (nRounds > 0) {
        c!addValue(random(50));
        await duration(1/4, 1/4);
        nRounds = nRounds - 1;
    }
    println(`Finished at $now()$`);
}

The main block records in-between advancing the clock by 1/4 (0.25).

Converting Time Values

To visualize times properly, they must be converted to a format that the visualization library expects.

In addition to the chosen offset (2020-04-01 in our case), we also need a scaling factor to convert the logical clock values of ABS to real date and time values. We choose to represent each day by an increment of 1; fractional values of the clock represent the time of day. Since our model advances the clock by 1/4 sixteen times, the values are recorded at 6am, 12pm, 6pm and midnight for four days.

As an example, here is a Javascript function that takes the startdate (as a string parseable via new Date) and offset (as a floating point number, with the integer part the number of days and the fractional part the time of day, with 0.5 exactly noon) and returns a Javascript Date object.

function absolute_date(startdate, offset) {
    let days = Math.floor(offset);
    let time = offset - days;
    let date = new Date(startdate);
    date.setDate(date.getDate() + days);
    date.setTime(date.getTime() + time * 24 * 60 * 60 * 1000);
    return date;
}

Visualizing the Time Series

The following is a complete HTML file that uses the Highcharts library to draw a line diagram of the random values generated by ABS. The function drawChart retrieves the starting offset and value list via the Model API, then creates a Highcharts chart with the downloaded values.

<!DOCTYPE HTML>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Date and Time in ABS</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://code.highcharts.com/highcharts.js"></script>
    <script src="https://code.highcharts.com/modules/exporting.js"></script>
    <script src="https://code.highcharts.com/modules/export-data.js"></script>
  </head>
  <body>
    <h1>Date and Time in ABS</h1>
    <div id="chart-container">
    </div>
  </body>
  <script type="text/javascript">
    'use strict';
    function absolute_date(startdate, offset) {
        let days = Math.floor(offset);
        let time = offset - days;
        let date = new Date(startdate);
        date.setDate(date.getDate() + days);
        date.setTime(date.getTime() + time * 24 * 60 * 60 * 1000);
        return date.getTime(); // number of milliseconds, as needed by Highcharts
    }

    function drawChart(){
        $.when(
            $.getJSON("/call/Calendar/getStartDate"),
            $.getJSON("/call/Calendar/getValues")
        ).done(function(resstartdate, resvalues) {
            let startdate = resstartdate[0].result;
            let data = resvalues[0].result.map(p => [absolute_date(startdate, p.fst), p.snd]);
            Highcharts.chart('chart-container', {
                type: 'line',
                title: { text: 'Dates and values from ABS' },
                xAxis: { type: 'datetime' },
                yAxis: { title: { text: 'Value (random number)' } },
                series: [{
                     name: 'random value', data: data
                 }]
            });
        });
    }

    $(document).ready(function(){
        drawChart();
    });
    
  </script>
</html>

Running the Example

The following small Makefile can be used to compile and start the model. Make sure to store the ABS model in a file CalendarTime.abs and the html in a file index.html, then type make run and connect the browser to http://localhost:8080.

.PHONY: all
all: run			## Generate data and start model (default)

.PHONY: compile
compile: CalendarTime.abs index.html ## Compile the model
	absc --erlang CalendarTime.abs --modelapi-index-file index.html

.PHONY: compile
run: compile			## Execute the model
	gen/erl/run -p 8080 -v

.PHONY: help
help:				## Display this message
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'