Expressions that don't array-abstract

Principle of Array Abstraction

The general principle of array-abstraction can be expressed as follows. Suppose f(x) denotes any function, expression, or sub-model that computes a result from input(s) x, and which does not operate over the index I. Then for any input x (which may be array-valued, and may contain the index I), and any element n of I, the following holds:

f(x[I=n]) = f(x)[I=n]

In English this says that if you compute your model on the nth slice of the input, you'll get the same thing you'd get if you compute your model on the entire array input and then keep just the nth slice of the result.

This simple concept turns out to be extremely powerful. Notice that the concept applies at every level -- at the level of individual function calls or individual operations (like +, *, etc), all the way up to the level of your entire model. The uniformity of array abstraction allows you to add and remove dimensions from our models easily, even after the logic is fully encoded, at which point you are better able to understand your problem and decide which subdivisions lead to the greatest insight. It also allows us to think abstractly about our problem, rather than worrying about the mechanics of computation, greatly simplifying cognitive load while building models.

Expert modelers learn the importance of ensuring that models fully array-abstract, so that you can add new dimensions at any time and feel confident that the correct results continue to be computed.

Limitations

In an ideal world, all expressions and models would obey the above principle of array-abstraction. In Analytica this is nearly the case; however, there are several cases which do not fully array abstract. These fall into a couple categories:

  • List-generation functions: These cannot accept array-valued parameters.
  • Expressions that assume scalar or 1-D parameters. These usually arise from omitting indexes unnecessarily -- by specifying the index you are operating over, your expressions can usually be made to array abstract.
  • While loops, where the condition is ambiguous when array-valued.
  • Assignments in If-Then-Else.
  • Functions that simply violate the array-abstraction principle. E.g. Size.

There may be cases where it is appropriate to use expressions that do not array-abstract, and even in these cases, with surrounding declaration qualifiers you may still be able to ensure that your net expressions abstracts. Nevertheless, a key skill for moving from a competent Analytica model builder to an expert model builder is learning to recognize these cases, and understanding how to formulate expressions so that they will array abstract. In most cases that do not array abstract, there are equivalent formulations possible that do abstract.

List-Generation Functions

Consider the function: Sequence(a,b), or equivalently a..b -- a sequence from a starting value «a» to an ending value «b» by ones. For example:

Sequence(1,5) → [1,2,3,4,5]
Sequence(3,8) → [3,4,5,6,7,8]

Now, suppose that b := [5,6,7] and you evaluate Sequence(1,b). You end up with the three sequences:

[1,2,3,4,5]
[1,2,3,4,5,6]
[1,2,3,4,5,6,7]

If you try to collect these along the index b, the result would be a non-rectangular array. Since Analytica does not allow non-rectangular arrays, this construct is allowed. Hence, Sequence is an example of a function that does not array-abstract. When you use it, you must guarantee that each parameter is scalar. If you add a dimension to your model that propagates to a parameter of Sequence, an error will occur when Sequence is evaluated.

This general limitation applies to all functions that evaluate to a list. These include:

When you need to use a function that returns a list result, and you need it to operate in an array-abstractable fashion, one method is to pad the shorter results with Null values. The functions Subset, SplitText and Concat provide an optional index parameter for the result, which does exactly this, thus saving you the work of re-indexing the intermediate results onto a known index. For list-generating functions that do not do this, you can accomplish this with your own code, often involving explicit For..Do loops.

Expressions assuming 1-D parameters

The array parameter for many array functions, including Sum, Max, Min, and others is optional but highly recommended. When you sum over an array, you should write Sum(A,I), including the second index parameter «I» to tell Analytica which index you are operating over.

When you know that your array «A» has only one dimension, most functions will allow you to omit the index parameter and write, e.g., Sum(A). While this works fine when A is 1-D, if an additional dimension is added to A in the future, the operation becomes ambiguous -- which dimension is being operated over. For legacy reasons, Analytica in many cases will allow such ambiguous operations without reporting an error, but since you can't tell which dimension is operated over, it may do something differently than expected. The best advice -- always specify the index being operated over (unless your intention is to operate over the implicit dimension, in which case the omission of the index parameter is fine).

While loop conditions

A While loop, e.g.:

While cond Do expr

evaluates expr repeatedly until cond becomes true. While requires cond to be scalar, so that the termination condition is non-ambiguous. Adding a dimension that propagates to cond may result in an error being reported by While that cond is not scalar.

Assignment inside If-Then-Else

The conditional construct If A Then B Else C in Analytica is evaluated in an array-fashion. This can lead to some non-intuitive behaviors, and in particular to expressions that violate the law of array-abstraction if you do anything with side-effects (e.g., assignment) inside the Then or Else clauses.

The following example demonstrates (for more information, see the "Gotchas" section at Assignment operator:

Var b := [true,false];
Var r := 0;
If b then r:=r+1 else r:=r-1;
r
→ 0

When this is evaluated, since b contains both true and false values, both the then and else clauses are evaluated. r is incremented then decremented, so the final result is 0. But:

Var b := true;
Var r := 0;
If b then r:=r+1 else r:=r-1;
r
→ 1
Var b := false;
Var r := 0;
If b then r:=r+1 else r:=r-1;
r
→ -1

If the expression obeyed the principle of array abstraction, the result for the b:=[true,false] case would be [1,-1], not 0.

Functions that violate the Array-Abstraction Principle

Some functions simply violate the law of array-abstraction by the very nature of what they do. Many meta-inferential algorithms fall into this category. Perhaps the most common yet subtle is the Size function. As an exercise, dhow that Size(A) violates the law of array-abstraction expressed at the top of this page.

Obtaining Abstractable Expressions via Encapsulation

Suppose you have an expression that works in the simple case, but does not array-abstract for one of the above reasons. Although you may be able to find a way to write it differently, you may also be able to encapsulate it appropriately so that the full expression does array abstract.

Consider the example above demonstrating the perils of using side-effects inside an If-Then-Else. In this case, we have a single variable, b, serving as input to the algorithm. By augmenting the declaration of b to indicate the dimensionality that b should have inside the expression, Analytica can iterate appropriate so that b will be atomic when the If condition is evaluated. The following two declarations are equivalent syntaxes for declaring that b is atomic (i.e., zero-dimensional):

Var b[ ] := ...

or

Atomic b := ...

The first syntax lists the allowed dimensions of b between brackets -- in this case, an empty list, indicating that no dimensions of b are allowed.

Var b[ ] := [true,false];
Var r := 0;
If b then r:=r+1 else r:=r-1;
r
→ [1,-1]

A second method for encapsulating, which is more efficient when there are two or more inputs, is to wrap the expression in a User-Defined Function and declare the dimensionality of each parameter:

Function F(b : atom) :=
  Var r := 0;
  If b Then r:=r+1 Else r:=r-1;
  r

When called with F([true,false]), Analytica uses the dimensionality information to iterate so at to obtain a result consistent with the principle of array-abstraction.

Comments


You are not allowed to post comments.