Meta-Inference
Meta-Inference means reasoning about the structure of a model, not just the values of variables -- for example, finding all the ancestors of a variable, or finding all variables inside a Module (and its submodules). Meta-inference is an advanced topic, appropriate only for experienced modelers with programming experience. It makes it possible to add functions to extend Analytica's features, for example:
- Create custom reports about a model -- for example, a table containing all the object descriptions and definitions, or reports printed to flat files with this information.
- To change node colors to reflect results of sensitivity analysis.
- Collecting time/memory profiling information about the objects in your model, as the
Profiler.ana
library. - Finding all the ancestors with a certain property, as does Make Importance.
- Running stochastic simulations in batches, so that huge models or huge sample sizes can be utilized within the constraints of available memory.
Objects and Handles
Normal modeling focuses on calculating the values of variables by evaluating their definitions. Meta-inference involves working with Analytica objects, such as Variables, Buttons, and Modules, and their attributes.
Code refers to an Object using a handle. The simplest way to get a handle to an object with identifier o
is with the Handle function:
Handle(o)
A related function gives you the handle to an object given its identifier as a text:
HandleFromIdentifier(text)
Lists and Indexes of Handles
Often, meta-inference algorithms manipulate lists of handles, or utilize indexes where the index elements are handles.
A global index can be defined as a list of identifiers. You can do this by selecting "List" from the definition type pulldown, and entering identifers of existing objects as the elements of the index. When you do this, the IndexValue of the index consists of handles to objects. If you define Index In1
in this fashion and evaluate IndexValue(In1), you will obtain a list of handles. Each of the element objects will be evaluated when your index is evaluated, so these must be well-defined variables.
A local index can be defined as a list of handles using the LocalIndex..Do construct. For example, the following defines an index, I
, as the objects contained in Module Mo1
, and computes an array based on this index containing all the descriptions of those objects:
LocalIndex I := Contains Of Mo1;
Description Of I
When you are performing meta-inference, you should generally use LocalIndex..Do rather than Index..Do. While both can be used, they differ in how the local index behaves when used in a Subscript operation. LocalIndex..Do can be used without the elements being evaluated, and hence is what is almost always desired. Since the two are equivalent when you aren't using handles, it is useful to always use LocalIndex..Do so you don't have to remember the distinction.
The Display of Handles
When a Handle is displayed in a result table, its title is shown in the table cell, unless Show By Identifiers is on, in which case its identifier is displayed. Show by identifier is toggled via the Object menu, or by pressing Ctrl+Y.
If you double click on a cell containing a handle, Analytica jumps to the object window for that object.
Treatment of Handles in Local variables
There are four constructs for declaring local variables (excluding local indexes) within an expression:
- Var x := ...; { deprecated }
- For x := ...;
- Local x := ...;
- LocalAlias x := ...;
When a handle is assigned to a local variable, the local variable may be treated either as a holder of a handle object, or as an alias to the object pointed to by the handle. The last two constructs (Local and LocalAlias) make this distinction explicitly. When «x» is declared using Local, it acts as a holder of a handle object, while when it is declared using LocalAlias, it acts as an alias of the object. When you are creating meta-inference algorithms, it is advisable to use these two constructs, rather than Var and For, when holding handles. You can assign handles to locals using Var and For, but the semantics is a hybrid between Local and LocalAlias. It is simply easier to learn and understand if you use Local and LocalAlias exclusively.
When using LocalAlias, the local variable acts as an exact alias for the object pointed to by the handle. For example, the following are exactly equivalent:
LocalAlias x := Handle(Va1) Do x^2
Va1^2
In general, if you take an expression of the form:
LocalAlias x := Handle(y) Do «body»
and then take only the «body» part and replace every occurrence of x in «body» with y, the resulting expression will be exactly equivalent.
When x is declared using LocalAlias, x must be the alias of exactly one object. Hence, if you assign a list of handles to x, the LocalAlias construct iterates over the handles, evaluating «body» each time. For example:
LocalAlias x := ListOfHandles(Va1, Va2, Va3) Do Size(x)
Evaluates Size(Va1), Size(Va2), Size(Va3)]
.
In other cases, you may want to hold a handle in the local variable, and treat it as a handle object. In these cases, use MetaVar.
Consider the following set of three variables:
Variable A := 5
Variable B := Local x := Handle(A) Do x
Variable C := LocalAlias x := Handle(A) Do x
The result of B
is a handle to A
. The result of C
is 5.
As another example, consider the following expression:
Finally, consider these:
Variable A := 3.1
LocalAlias x := Handle(A) Do TypeOf(x) → "Number"
Local x := Handle(A) Do TypeOf(x) → "Variable"
When this expression is in a User-Defined Function that is called from a button script, once the code shown is evaluated, x
holds a handle to C
(and the variable C
is unchanged), while y
is still an alias of B
by B
is now defined as D
.
You can loop over a collection of handles using Local by declaring dimensionality, using an empty list of indexes to list over atomic elements. For example:
Local x[] := ListOfHandles(Va1, Va2, Va3) Do «body»
will evaluate «body» three times with x
holding a handle to Va1
, then a handle to Va2
, and finally a handle to Va3
.
Sometimes you may have a Local containing a handle or a list of handles, and you'd like to create an alias to that object. This is often useful when you want to use the object as an index. You can do this by assigning the Local to a LocalAlias.
Local s := A { hold the sum }
Local inds := IndexesOf(A); { This is a list of handles to the indexes of A}
LocalAlias J := inds; { This iterates over the indexes }
s := Sum(s, J)
You can convert between Local and LocalAlias semantics using the Handle(..) and Evaluate(..) functions. For example, if you have a local variable, x, declared using LocalAlias, and you find yourself needing the handle instead, use Handle(x). Conversely, if you have a local declared using Local and you want the value of the underlying object, then use Evaluate(x). The one exception is if you need to use it as an index, in which case you should declare another local using LocalAlias as shown in the previous example.
When defining a User-Defined Function, various Function Parameter Qualifiers can also be used to control whether the local variable acts according to the Local or the LocalAlias semantics. When you declare a variable using the Handle qualifier, this specifies that the data type of the value held is a Handle -- endowing the local variable with Local semantics. When you use the Variable or Object qualifier, then the parameter acts as an alias.
Evaluation
Subscripting
Suppose an index contains a list of handles, e.g.:
Index I := ListOfHandles(Va1, Va2, Va3, Va4)
And you have an array, A
, that is indexed by I
. You want to get the slice of A
corresponding to Va2
. Then the correct way of doing this is:
A[I = Handle(Va2)]
You may be tempted to write A[I = Va2]
, but this will only work if Va2
contains a single number, and in fact may not work correctly in that case. Recall that when you write A[I = Va2]
, when this is evaluated Va2
is first evaluated and its value is passed to the subscript operation. Hence, if Va2
is defined as 3, then A[I = Va2]
is equivalent to writing A[I = 3]
. Subscript will in fact use the value 3, match it to the value of Va3
, and return the second slice, unless Va1
or Va3
happens to also evaluate to 3. In that case, you have an ambiguity and not get the slice you intended. If Va2
is array-valued, then that value of 3 will not be matched to the value of Va2
, and again, A[I = Va2]
will fail to return the desired slice. Hence, to subscript by a handle, use the above method.
If you have a Local containing a handle, then you can use it directly. The following works fine:
When an index contains handles, Subscript will perform an associative lookup using either the handle or the value of the variable for the variables that evaluate to scalar values. This is useful, since it makes it possible to have an index containing constants, such as [Pi, True, False, INF]
, and then use A[I = Pi]
, for example. However, in meta-inference algorithms this can be a disadvantage. First, to do any associative lookup, Analytica must evaluate every object in the list to see if it has a scalar value. Not only can this be time consuming, but you may have objects that aren't defined yet or have evaluation errors. Second, if some of those objects evaluate to handles themselves, you can create additional ambiguities. Thus, in meta-inference, it is often preferable to turn off the use of underlying values entirely. To do this, we flag an index as meta-only. For a global index, you can do this by checking the meta-only attribute. For a local index, you can do this by using the Local..Do constraint rather than the Index..Do construct.
Suppose Va3 := 3
. Consider:
Local I := ListOfHandles(Va1, Va2, Va3, Va4);
Index J := ListOfHandles(Va1, Va2, Va3, Va4);
Local A := @I + @J;
A[I = Handle(Va3)] { Good -- works }
A[J = Handle(Va3)] { Good -- works }
A[I = 3] { Fails -- I is meta-only, so can't look up using values}
A[J = 3] { Maybe ok -- returns the J = Handle(Va3) slice, unless an ambiguity exists }
A[I = Va3] { Fails -- I is meta-only, so can't look up using values}
A[J = Va3] { Maybe ok -- returns the J = Handle(Va3) slice, unless an ambiguity exists }
The simple heuristic -- in a meta-inference algorithm, if you are traversing a model, use Local, and if you are using a global index to store a list of objects of interest, check its meta-only flag.
Accessing attributes
Attribute values as accessed using the «Attrib» Of «Obj» operator. See Attrib of Obj for details. You can use a Local-declared name or a LocalAlias-declared name in the «obj» position with the same effect.
Notable attributes
Contains, IsIn, Outputs, Inputs, DisplayOutputs, DisplayInputs, Identifier, Class, nodeSize, nodeLocation, nodeColor, nodeFontColor.
Manipulating Local Indexes
See IndexesOf.
Parameters and Local Variables holding handles
Assignment
Suppose a local variable declared using Local holds a handle. If you assign a different handle to the object, it simply changes the value stored in the local variable to a different handle. The object pointed to is not impacted. For example:
This code finds the top-level object in the module hierarchy (i.e., the Model object). The key thing here is the assignment on the third line. Each time the assignment is hit, v
is changed to a handle pointing to the parent of the previous object. This continues until it reaches the object with no parent.
When a local variable is declared using LocalAlias, an assignment will attempt to change the underlying object definition. For example:
After this code executes, the local v
is still a handle to Va1
, but the definition of Va1
is now Va2
. This code would issue an error if attempted from a variable definition, because it contains a global side-effect (changing another global variable), which is disallowed from a variable definition (unless that variable is defined as ComputedBy(..)). It would, however, work as described if called from a button script.
Looping over Handles
When you have a list or array of handles, the following two constructs should be used for explicit looping within an expression:
LocalAlias i := myListOfHandles Do «body»
Local i[] := myListOfHandles Do «body»
In both cases shown, i
will be literated over all elements of myListOfHandles
(or of myArrayOfHandles), and for each value, «body» is evaluated. The difference is the two constructs is the semantics of the i
. When LocalAlias is used, i
takes on an alias semantics (it acts exactly as if it is the object that the handle points to), while when Local is used, i
is a local variable holding a handle object.
A second way to accomplish looping is by utilizing dimensional declarations in User-Defined Function parameters. For example:
Function CountObjs(v: handle atom) := 1 + If IsNull(Contains of v) Then 0 Else 1 + Sum(CountObjs(Contains Of v))
When passed a handle to a module, this recursive function counts the number of objects in that module. Iteration is accomplished through the use of the atom qualifier in the parameter declaration. When passed a list of child objects, i.e., Contains of v
, the function is called once for each object in the list.
See Also
- Category:Meta-Inference Functions
- Handle function
- HandleFromIdentifier
- IndexesOf
- Variable parameter qualifier
- MetaIndex..Do
- Attrib of Obj
- FindObjects
Enable comment auto-refresher