|
<< Click to Display Table of Contents >> Timeseries |
![]() ![]()
|
The internals of timeseries objects are described in more detail in this section.
Here, we will look more into timeseries types, array-timeseries, and timeseries arithmetics. Also, the so-called "lag problem" will be mentioned.
The Series class implements the IVariable interface and defines all kinds of Gekko timeseries. The timeseries come in four subtypes, defined as the field .type (the enumeration ESeriesType).
name meta dataArray dimensions dimensionsStorage
----------------------------------------------------------------------------
Normal yes yes yes 0 no
Light no no small 0 no
Timeless yes yes length 1 0 no
ArraySuper yes yes no >= 1 yes
----------------------------------------------------------------------------
Array-subseries. Has no name, else is completely like Normal type.
These types (timeseries variants) could have been defined in their own classes, all implementing the IVariable interface. But that would have produced a lot of IVariable types, and a lot of code to handle for instance the addition of two kinds of timeseries. The timeseries types are simply too similar for this to pay off, so instead the types use the same Series class, with some twists as seen in the table above.
•Normal. This is a normal timeseries that can be stored in a databank. It is basically like this. It has a name field, and a sub-object that contains meta-information like label, source, etc. It has a normal data array, typically of length 200 to start out with. Does not contain dimensions or array-subseries.
•Light. This is an intermediate "helper" timeseries object. It has no name, and no meta-information object. When for instance performing the addition x1+x2, the result will be stored in a light timeseries. Light timeseries cannot be stored in databanks, and this is not their purpose. Their dataArray is as compact as possible, only containing the relevant sample period. The intention is that it is fast to create and alter, without anything superfluous.
•Timeless. A special kind of timeseries where the dataArray always has length 1, and where querying for any observation of the relevant frequency always returns the value of that dataArray. Such a timeseries is functionally equivalent to a ScalarVal (Gekko value %v), but it can be practical to be able to use a constant-value timeseries. Some GAMS gdx datasets contain such variables where there is no time dimension.
•ArraySuper. This is a container for array-subseries, for instance if we have the array-series x (the array-superseries), which contains the element x[a] (the array-subseries). ArraySuper has name and meta-information, but no dataArray. It has an integer that describes the number of dimensions (>0), and a dimensionsStorage object that stores the sub-series (this object acts much like a C# Dictionary).
•Array-subseries is not a distinct type, just note that array-subseries are normal timeseries (Normal type) that have no names given (their .name field = null).
More about array-series
Some more explanation regarding array-series: In a series of ArraySuper type, the field dimensionsStorage will store the sub-series. This object is a Multidim object, which stores the items in a dictionary: Dictionary<MultidimItem, IVariable>. Here, MultidimItem is the key in the dictionary, and even though for the array-subseries x[a, b], a string key like "a, b" could in principle be used, it is better and more performant to use another description of the keys. MultidimItem basically contains a string array (string[]), which in this case would be {"a", "b"}, and MultidimItem implements the two methods GetHashCode() and Equals(). This makes it possible for the dictionary Dictionary<MultidimItem, IVariable> to use MultidimItem objects as keys. The MultidimItem class also contains a method GetName(), which will in "our" case use information from the parent object (each MultidimItem points back the the ArraySuper series) and the elements inside the MultidimItem object to provide a string like for instance x[a, b] for use when printing etc. Now, the Dictionary<MultidimItem, IVariable> stores IVariables, or more specifically Series objects in our case. So when stating for instance prt x[a, b];, Gekko will construct a MultidimItem with the string array {"a", "b"}, and use this to find the corresponding sub-series in the dictionary.
All Series object have a .mmi object derived from MultidimItem. It is only active (non-null) for array-subseries, where it just points to the MultidimItem object used in the Dictionary<MultidimItem, IVariable> dictionary. This way, when handling a array-subseries, the subseries "knows" which MultidimItem it is bundled with, and hence also the dimension elements it represents (for instance [a, b]).
An illustration:

Timeseries arithmetics
As mentioned in the overview section regarding Series.cs, timeseries arithmetics are handled in a certain way. The fundamental issue regarding this is that when a mathematical transformation is performed on for instance two timeseries (like for instance the addition x1+x2), this is done by first finding the raw data arrays inside the series x1 and x2 respectively, aligning these regarding the time period, and performing the addition on the "low level" data. That is, simply looping over the two data arrays and putting the result into another data array. The code would look like this in C#:
for (int i = 0; i < GekkoTime.Observations(window1, window2); i++) { |
Here, arraya is the double[] array that receives the data, and this array is later on transferred to the resulting timeseries (or to a light timeseries for an intermediate result). The calculation runs over i = 0 to the number of observations in the "window" of the data that is going to be calculated. The integers ib1 and ic1 are offsets that make sure that the values accessed inside the two arrays represent the same date.
Now, this would work fine for additions, but there are also subtractions, multiplications, etc., which would produce a lot of repetitive code. Therefore, such arithmetics involving timeseries are handled more generically with Func<> delegates, for instance:
for (int i = 0; i < GekkoTime.Observations(window1, window2); i++) { |
where a() represents the mathematical function. There are the following "arithmetics" methods, all residing in Series.cs:
•ArithmeticsArraySeriesArraySeries(), performing math directly on two array-series
•ArithmeticsArraySeriesSeries() and ArithmeticsSeriesArraySeries(): performing math involving an array-series and a normal series
•ArithmeticsArraySeriesVal(), performing math involving an array-series and a value.
•ArithmeticsSeries(), like -x, log(x), etc.
•ArithmeticsSeriesLag(), like pch(x), dif(x), etc.
•ArithmeticsSeriesSeries(), like x1 + x2, etc.
•ArithmeticsSeriesVal(), like like x + %x.
The first three methods handle algebra that involves array-series. Although a bit undeveloped, it is for instance possible to add two array-series (if their elements conform), and you may for instance also use x/1000 where x is an array-series to quickly divide each element with 1000. Such capabilities can be practical. The rest of the methods deal with a single series, a single series with lag included, two series, or a series and a value.
All these methods use Func<> delegates, so that the particular math function (for instance addition, subtraction, multiplication, etc.) can be stated in form of an argument a that can be used like a(...), for instance a(arrayb[i + ib1], arrayc[i + ic1]) in the case where two series are handled.
These methods help keeping Gekko performant. Timeseries operations on timeseries over long sample periods are common, and the most performant way of doing such calculations is obviously to do them at the lowest data array level.
Timeseries operators
Another kind of operators are statements like y <p>= 5;, y += 1;, pch(y) = 5;, etc.
These are handled in OperatorHelperSeries(), OperatorHelperSequence(), OperatorHelperScalar(), all from the O.cs file. These all call the OperatorHelper1() and OperatorHelper2() method, where the latter performs the actual transformation. Like for instance if the operator is percentage growth (like in y <p>= 5; or pch(y) = 5;), the following is done:
lhsData[i] = lhsData[i - 1] * (1 + rhsData[i] / 100d); |
Here, the input data has been transformed to simple arrays, and the code simply tells us the equivalent to the expression y = y[-1] * (1 + x/100), where x is the percentage growth.