Computation in Pictorus

Currently, data that flows between blocks are uniformly represented as 64-bit floating point values. In the generated Rust code, this translates to the f64 type. This is a fast and convenient choice for simulation, but there are some possibly surprising implications.

As always with computer floating point arithmetic, only a finite set of numbers can be represented, so expressions that should be equal mathematically, may not be exactly equal numerically.

For example, in Rust,

if 0.1 + 0.2 == 0.3 {
    println!("A");
} else {
    println!("B");
}

Will this print A or B? If you said, B, you're correct. The reason is interesting, but what's most important is that we need to understand when calculations are exact and when they may not be.

In addition to the usual challenges of numerical analysis, integer arithmetic and logical operations require care.

Exact Arithmetic

The range of integers that can be represented exactly in 64-bit floating point is \(-2^{53} \) to \(+2^{53} \) or a little over 15 decimal digits. Addition, subtraction, and multiplication of integers in this range remain exact provided the result does not exceed the range. Division is exact if the result has a remainder of zero. Understanding exact computations is key to understanding when it is safe to compare values with strict equality.

As described in True and False in a Floating Point World, False is represented as exactly 0.

Sources of Error

There are several sources of numerical error relevant to numerical control systems. A full treatment of Numerical Analysis is beyond the scope of this documentation, however, some hints and references are provided.

  • Sampling error
  • Data uncertainty
  • Roundoff error
  • Underflow/overflow
  • Division by zero and other exceptional conditions

Sampling Error

To test a threshold crossing, due to the possibility of sampling error, it is unreliable to use an exact comparison. It is necessary to test for a value in a range or react to the first value that exceeds a given point. True and False in a Floating Point World provides an example.

When analyzing sample signals from a continuous process, the sampling rate must be high enough to avoid aliasing. The FFT block and the Frequency Filter block may be useful.

Data Uncertainty

Actual sensor data typically includes noise. It may be helpful to

Roundoff Error

Pictorus uses the highest available precision to minimize the effects of roundoff error, however, it is still necessary to avoid unstable numerical algorithms and ill-conditioned problems for which relatively small input changes lead to large output changes.

Underflow/overflow

Consider an approximation algorithm that terminates when an error expression is less than an absolute threshold. Although a threshold may be representable, if the threshold is too small, the algorithm may not terminate. Subtracting a very small floating point value from a very large floating point value may result in a difference exactly equal to the very large value. From this point on, no progress is made. It is often most productive to test the error relative to the magnitude of the numbers being compared.

IEEE 754 floating point values have the maximum precision between one and negative one.

With 64-bit floating point values, overflows are uncommon for practical examples, but it is good to consider how large numbers might enter and corrupt a computation. For example, a sensor may emit invalid data when an error condition occurs. If there is no error condition signal, or the error condition signal is not used, the input data may contain unreasonably large values. A Deadband block can be used to detect a signal out of range by treating its output as a not-valid signal.

Division by Zero and Other Exceptional Conditions

Currently, Pictorus prioritizes avoiding crashes over generating mathematically correct results. Division by zero and other undefined calculations are arbitrarily defined to be zero. Blocks that potentially mask calculation errors:

  • Derivative Block - Since the derivative block requires Max Samples to compute the derivative, it will output zero for the initial timesteps.
  • Equation Block - If the computation is undefined at for certain inputs, the corresponding output is zero. For example, 1/x0 will be zero when x0 is zero.
  • Exponent Block - We define \(0^{0} = 0 \). See Zero to the power zero.
  • FFT Block - With a Buffer Size of zero, an empty vector is produced.
  • Frequency Filter Block - With a Cutoff Frequency of zero, the filter passes the input almost unchanged. Changes are on the order of \(10^{-15} \).
  • Logical Block - See "True" and "False" in a floating point world and Logical Block.
  • Product Block - When the Method is ComponentWise and a Port Method is Divide, division by zero results in zero.
  • Rust Block - Aside from the guarantees provided by the Rust compiler, there are no additional guards provided for user-supplied Rust code.