|
<< Click to Display Table of Contents >> Expressions |
![]() ![]()
|
Expressions can be, for instance, an expression like pch(x[-1] + 1)[2020]. In such an expression, there is a lag ([-1]), an addition (+), a function (pch()), and an indexer ([2020]). Expressions are quite fundamental in Gekko. A Gekko expression always produces an IVariable as result, that is, a variable of one of the types Series, ScalarVal, ScalarDate, ScalarString, List, Matrix, or Map.
The lag problem
One thing to note about expressions is that if they involve timeseries, there is the so-called lag problem. More about this problem in the blog post here. As mentioned in the previous section, the ANTLR parser and AST tree walker transforms Gekko statements into corresponding C# code. Let us take a look at the C# code corresponding to the expression pch(x1+x2), which is the percentage growth of the timeseries x1+x2. The generated C# code looks like the following:
Functions.pch(O.Smpl(smpl, -1), smpl, null, null, O.Add(smpl, O.Lookup(smpl, null, null, "x1", null, null, new LookupSettings(), EVariableType.Var, null), O.Lookup(smpl, null, null, "x2", null, null, new LookupSettings(), EVariableType.Var, null))); |
Let us rearrange this into some parts:
Functions.pch( O.Smpl(smpl, -1), smpl, null, null, O.Add( smpl, O.Lookup(smpl, null, null, "x1", null, null, new LookupSettings(), EVariableType.Var, null), O.Lookup(smpl, null, null, "x2", null, null, new LookupSettings(), EVariableType.Var, null) ) ); |
The pch() function itself has the following definition:
public static IVariable pch(GekkoSmpl2 smplOriginal, GekkoSmpl smpl, IVariable _t1, IVariable _t2, IVariable x1) |
So it expects five arguments: smplOriginal, smpl, _t1, _t2, and the "real" argument, x1. The _t1 and _t2 variables are for setting a local time period with for instance pch(<2010 2020>, x) (which is not used here, so these are null). And x1 is just the input variable that is used. So what is smplOriginal and smpl in the definition?
These are similar objects: the first is derived from the class GekkoSmpl2, and the second from the classe GekkoSmpl. To start with the latter, this is an object that contains information on primarily the sample period (time period) that timeseries are to be evaluated over. But the GekkoSmpl class also contains other things, of which the most important is probably the field bankNumber. This is 0 when the object is created, which means "do nothing". But if it is set to 1, all timeseries without an explicit databank indicator (like b:x), will be taken from the Ref databank, and will not be searched for elsewhere. This is handy when doing for instance a prt <m> x;, which is essentially the same as prt x - ref:x;.
But otherwise, the most important fields of GekkoSmpl are t0, t1, t2, and t3. Here, t1 and t2 are the periods actually used when, for instance, printing something. So for instance, when doing prt <2010 2020> pch(x1+x2);, we will have t1 = 2010 and t2 = 2020 (the periods are really GekkoTime structs, but that is a detail). So what are t0 and t3 for? This is the period that the inside of the pch(x1+x2) transformation is supposed to run over, in order to print the percentage growth of x1+x2.
Imagine that we want to print for the period 2010-20. If we calculate a new temporary timeseries x = x1+x2 over the period 2010-20, we will have a problem when we try to print pch(x) over 2010-20, because the expression contains an implicit lag (the lag [-1] in ((x1+x2)/(x1[-1]+x2[-1])-1)*100), and therefore needs the value of the sum x1+x2 in 2009 too, to print the percentage growth pch(x1+x2) in 2010.
To deal with this, the first two arguments of the pch() function call are O.Smpl(smpl, -1) and smpl, respectively. If the period is 2010-20, when the pch() expression is called, we will have the smpl object containing t0 = t1 = 2010, and t2 = t3 = 2020. Then the first argument is evalutated, that is, O.Smpl(smpl, -1). This does the following:
public static GekkoSmpl2 Smpl(GekkoSmpl smpl, int i) |
It constructs a GekkoSmpl2 object which remembers the t0 and t3 values from the original smpl object, and afterwards it adjust t0 of the smpl object (deducts 1 from it in this case). So before the call O.Smpl(smpl, -1), smpl.t0 would be = 2010, and smpl.t3 would be = 2020, but after the call, smpl.t0 would be = 2009, and smpl.t3 would be = 2020.
Because in C# all arguments are first evaluated before the actual method is called, the new value of smpl.t0 will now be used when the rest of the arguments are evaluated. This applies to the x1+x2 part of the expression call:
O.Add( smpl, O.Lookup(smpl, null, null, "x1", null, null, new LookupSettings(), EVariableType.Var, null), O.Lookup(smpl, null, null, "x2", null, null, new LookupSettings(), EVariableType.Var, null) ) |
When the addition is done, a temporary timeseries is created with the result, and because O.Add() here will be called with the value of smpl.t0 = 2009, the Add() code will make sure that the addition is started already in 2009, and that the resulting temporary timeseries obtains a value (the sum x1+x2) for all the periods 2009-2020.
When the pch() function starts up (Functions.pch()), it will therefore be fed with the following arguments:
•GekkoSmpl2 smplOriginal with .t0 value = 2010.
•GekkoSmpl smpl with .t0 value = 2009.
•IVariable _t1 = null
•IVariable _t2 = null
•IVariable x1 = a timeseries defined over the sample 2009-2020.
Now, inside the pch() function in C#, there is the following method call at the top of the method:
Program.RevertSmpl(smplOriginal, smpl); |
What this RevertSmpl() method does is that it transfers the .t0 and .t3 values from the smplOriginal object to the smpl object. Remember that smplOriginal.t0 was = 2010, so RevertSmpl() transfers this value to smpl.t0 (where the value is 2009 when the inside of the Functions.pch() method starts to run). Or in other words, the smpl.t0 value (and smpl.t3 if it had been changed) is reverted to what it was when the whole pch() method was called originally, that is, before the arguments of the method were evaluated.
So to conclude about the "lag problem", what the O.Smpl(smpl, -1) argument does is in reality that it adjust smpl.t0 for the remainder of the arguments, including the argument that has to do with the addition x1+x2. But when the method proper is called, RevertSmpl() at the beginning of the method makes sure that smpl is reverted to what it was originally.
Other considerations
The most complicated part of expression handling is timeseries. In Gekko 3.0, these are handled much like vectors, so that when performing for instance the expression x1+x2, instead of looping over the time period in an outer loop, using GekkoTime iterations, the two series are added more like two vectors or raw arrays.
In databanks, the timeseries are stored as Series objects, but intermediary results when evaluating expressions are stored in a Series object that is particularly simple, namely with the .type field = ESeriesType.Light. These "light" timeseries only store the most necessary parts, most notably the array that contains the data (and the size of this array will correspond to the sample size). For light timeseries, all meta-information is dropped, so these light series are really bare-bones. This is to make them performant and fast to create, because large expressions may produce a lot of such intermediate light timeseries (for instance, for the sum x1+x2+x3+x4+x5, four intermediate timeseries are created while constructing the sum).
Mathematical operators are generally translated into corresponding object methods. For instance, x1+x2 is basically translated into x1.Add(x2), x1*x2 into x1.multiply(x2), where x1 and x2 are two Gekko C# variables (implementing the IVariable interface). The IVariable interface makes sure that these methods are always available, but the methods may sometimes issue errors (if for instance the user tries to add two scalar dates, which is illegal).
Non-timeseries are in general easier to handle in expressions, since they do not depend upon sample periods. Scalars (values, dates, strings) are particularly easy, and regarding lists and maps, it should be noted that these can contain Gekko variables inside themselves. Lists are pretty straightforward, containing Gekko variables (with IVariable interface) in each element. Maps are much like databanks, and maps therefore implement both the IVariable and IBank interfaces. The latter makes sure that maps work like (mini-)databanks, just with a different syntax.
Finally, there are matrices. These can only contain values, for speed reasons. If you need a 2D table structure with different kinds of variables (for instance a table containing values, dates, and strings), you can use a nested Gekko List.
An expression of any kind and length always evaluates to a Gekko variable (IVariable). In assignments, the expression is on the right-hand side, and the receiving variable is on the left-hand side. More on this in the section on assignments.