Creating operations

Sometimes the operations contained in TimeDag will not be sufficient. This document explains how new operations can be created.

Standard alignment behaviour

In many cases, one wishes to write an op that specifies how to process one or more input values, whilst using default alignment semantics. In this case, one should use TimeDag.apply, wrap or wrapb.

TimeDag.applyFunction
apply(f::Function, x; out_type=nothing, time_agnostic=true)
apply(
    f::Function, x, y[, z, ..., alignment=DEFAULT_ALIGNMENT];
    out_type=nothing, time_agnostic=true, initial_values=nothing
)

Obtain a node with values constructed by applying the pure function f to the input values.

Iff time_agnostic is false, f will be passed the time of the current knot as the first argument, in addition to any values.

With more than one nodal argument, alignment will be performed. In this case, the alignment argument can be specified as one of INTERSECT, LEFT or UNION. If unspecified, DEFAULT_ALIGNMENT will be used.

Internally this will infer the output type of f applied to the arguments, and will also ensure that subgraph elimination occurs when possible.

If out_type is not specified, we attempt to infer the value type of the resulting node automatically, using output_type. Alternatively, if out_type is given as anything other than nothing, it will be used instead.

If initial_values is specified, it should be a tuple of the same length as the number of node-like arguments passed in. See Initial values for more details.

source
TimeDag.wrapFunction
wrap(f::Function; time_agnostic=true)

Return a callable object that acts on nodes, and returns a node.

It is assumed that f is stateless (this will therefore not work with closures). We also assume that we will always emit a knot when the alignment semantics say we should — thus f must always return a valid output value.

Iff time_agnostic is false, f will be passed the time of the current knot as the first argument, followed by the value of every input.

If the object is called with more than one node, alignment will be performed. If an alignment other than the default should be used, provide it as the final argument.

Internally this will call TimeDag.apply(f, args...; kwargs...); see there for further details.

source
TimeDag.wrapbFunction
wrapb(f::Function; time_agnostic=true)

wrapb is like wrap, however f will be broadcasted over all input values.

source

Creating sources

In order to create a source — i.e. an op with zero inputs — one should use the Low-level API.

Low-level API

The most general way to create an op is to create a structure that inherits from TimeDag.NodeOp, and implements both of:

One must use this to implement source nodes, but in other cases it is typically preferable to use Standard alignment behaviour. This is because there are number of rules that must be adhered to when implementing TimeDag.run_node!, as noted in its docstring.

Warning

Some care must be taken when implementing == for a new NodeOp. We require that == means "produce identical output for the same inputs". For example, two NodeOps must never compare equal if they have different value_types.

Failure to adhere to this will cause confusing behaviour in the Identity map. This is because, if calling TimeDag.obtain_node, it might think the node already exists, even though the NodeOps aren't truly equivalent.

Example: stateless source

Here is stateless source node, which is effectively a simplified iterdates:

struct MySource <: TimeDag.NodeOp{Int64} end

# Indicate that our source doesn't have any evaluation state.
TimeDag.stateless(::MySource) = true

function TimeDag.run_node!(
    ::MySource, 
    ::TimeDag.EmptyNodeEvaluationState, 
    time_start::DateTime, 
    time_end::DateTime,
)
    # We must return a Block with data covering the interval [time_start, time_end).
   
    # Here we define a node which ticks every day at midnight.
    t1 = Date(time_start) + Time(0)
    t1 = t1 < time_start ? t1 + Day(1) : t1

    # Figure out the last time to emit.
    t2 = Date(time_end) + Time(0)
    t2 = t2 >= time_end ? t2 - Day(1) : t2

    # For no particular reason, we 
    times = t1:Day(1):t2
    values = ones(length(times))
    return Block(times, values)
end