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.apply
— Functionapply(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.
TimeDag.wrap
— Functionwrap(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.
TimeDag.wrapb
— Functionwrapb(f::Function; time_agnostic=true)
wrapb
is like wrap
, however f
will be broadcasted over all input values.
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:
TimeDag.run_node!
TimeDag.create_evaluation_state
, OR definesTimeDag.stateless
to be true.
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.
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 NodeOp
s must never compare equal if they have different value_type
s.
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 NodeOp
s 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