Calculator

Introduction

The Calculator filter is a fast way to add derived data arrays to a dataset.
These arrays can be defined over points, cells, or just field data that is “uniform” across the dataset (i.e., constant over all of space).
Values in the array(s) you create are specified in terms of existing arrays via a function you provide.

The Calculator filter is similar in spirit to VTK’s array calculator, so it should be familiar to VTK users but the syntax takes advantage of JavaScript’s flexibility.

Usage

There are two methods provided for configuring the filter:

  • a simple method that assumes you will only be adding a single array defined on points (or cells) and written in terms of other arrays already present on the points (or cells).

    const calc = vtkCalculator.newInstance();
    calc.setFormulaSimple(
    FieldDataTypes.POINT, // Operate on point data
    ['temp', 'press', 'nR'], // Require these point-data arrays as input
    'rho', // Name the output array 'rho'
    (temp, press, nR) => press / nR / temp); // Apply this formula to each point to compute rho.
  • a more general method that allows multiple output arrays to be defined based on arrays held by different dataset attributes (points, cells, and uniform field data), but which requires a more verbose specification:

    const calc = vtkCalculator.newInstance();
    calc.setFormula({
    getArrays: (inputDataSets) => ({
    input: [
    { location: FieldDataTypes.COORDINATE }], // Require point coordinates as input
    output: [ // Generate two output arrays:
    {
    location: FieldDataTypes.POINT, // This array will be point-data ...
    name: 'sine wave', // ... with the given name ...
    dataType: 'Float64Array', // ... of this type ...
    attribute: AttributeTypes.SCALARS // ... and will be marked as the default scalars.
    },
    {
    location: FieldDataTypes.UNIFORM, // This array will be field data ...
    name: 'global', // ... with the given name ...
    dataType: 'Float32Array', // ... of this type ...
    numberOfComponents: 1, // ... with this many components ...
    tuples: 1 // ... and this many tuples.
    },
    ]}),
    evaluate: (arraysIn, arraysOut) => {
    // Convert in the input arrays of vtkDataArrays into variables
    // referencing the underlying JavaScript typed-data arrays:
    const [coords] = arraysIn.map(d => d.getData());
    const [sine, glob] = arraysOut.map(d => d.getData());

    // Since we are passed coords as a 3-component array,
    // loop over all the points and compute the point-data output:
    for (let i = 0, sz = coords.length / 3; i < sz; ++i) {
    const dx = (coords[3 * i] - 0.5);
    const dy = (coords[(3 * i) + 1] - 0.5);
    sine[i] = dx * dx + dy * dy + 0.125;
    }
    // Use JavaScript's reduce method to sum the output
    // point-data array and set the uniform array's value:
    glob[0] = sine.reduce((result, value) => result + value, 0);
    // Mark the output vtkDataArray as modified
    arraysOut.forEach(x => x.modified());
    }
    });

Formula (set/get)

An object that provides two functions:

  • a getArrays function that, given an input dataset, returns two arrays of objects that specify the input and output vtkDataArrays to pass your function, and
  • an evaluate function that, given an array of input vtkDataArrays and an array of output vtkDataArrays, populates the latter using the former.

When the filter runs, the arrays specified by getArrays are located (on the input) or created (and attached to the output data object) and passed to the evaluate function in the order specified by getArrays.

FormulaSimple (set)

Accept a simple one-line format for the calculator.
This is a convenience method that allows a more terse function to compute array values, similar to the way VTK’s array calculator works.
You may use it assuming that

  • you only need to generate a single output data array; and
  • your formula will only use arrays defined at the same location of the input as the output array (i.e., if the output will be point-data, only point-data arrays and point coordinates may be passed to the formula; similarly, cell-data outputs may only depend on cell-data inputs).
    This method calls setFormula() with an object that includes the information you pass it.

The setFormulaSimple method takes:

  • a location specifying where on the input and output datasets the arrays are defined;
    the location value should be a FieldDataTypes enum (see Common/DataModel/DataSet/Constants.js).

  • an array containing the names of input arrays the formula will need to compute the output
    array values. These arrays must exist on the input dataset at the given location or they will be
    reported as null when the formula is evaluated.

    Note that when the location is FieldDataTypes.POINT, the coordinates of each point will be automatically appended to the list of arrays you request.

  • the name of the output array to create, which will be defined at the same location as the input arrays.

  • a formula (i.e., a JavaScript function) which takes as its input a single tuple of each input
    array specified and generates a single output tuple.
    Note that when input arrays you request have a single component, formula will be called with a single number;
    when input arrays have multiple components, your formula will be called with an array of values.
    As an example, if you pass ['density', 'velocity'] as the second argument, where pressure is a 1-component array and velocity is a 3-component array, then a formula that computes the momentum’s magnitude would be written as

    (rho, vel) => 0.5 * rho * (vel[0]*vel[0] + vel[1]*vel[1] + vel[2]*vel[2])
    
  • a dictionary of options that can be used to change how the formula is used:

    • if numberOfOutputComponents is specified and greater than 1, then the formula should return a tuple instead of a single number each time it is called.
      To improve speed when this option is specified, a pre-allocated tuple is passed into your formula as the final argument.
      Your function can fill this array and return it as the output value to avoid the overhead of allocating a new array each time the formula is called.
    • if outputAttribute is specified as an AttributeTypes enum (see Common/DataModel/DataSetAttributes/Constants.js), then the output array will be marked as the given attribute type.
      If unspecified, then AttributeTypes.SCALAR is assumed.

Source

index.js
import vtk from 'vtk.js/Sources/vtk';
import macro from 'vtk.js/Sources/macros';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkPoints from 'vtk.js/Sources/Common/Core/Points';
import { FieldDataTypes } from 'vtk.js/Sources/Common/DataModel/DataSet/Constants';
import { AttributeTypes } from 'vtk.js/Sources/Common/DataModel/DataSetAttributes/Constants';

const { vtkWarningMacro } = macro;

// ----------------------------------------------------------------------------
// vtkCalculator methods
// ----------------------------------------------------------------------------

function vtkCalculator(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkCalculator');

publicAPI.setFormula = (formula) => {
if (formula === model.formula) {
return false;
}
model.formula = formula;
publicAPI.modified();
return true;
};

publicAPI.getFormula = () => model.formula;

publicAPI.augmentInputArrays = (locn, arraysIn) => {
const arraysOut = arraysIn.slice(0); // shallow-copy the inputs
// Make point coordinates available whenever the field-data is associated with
// points or graph vertices:
if (locn === FieldDataTypes.POINT || locn === FieldDataTypes.VERTEX) {
arraysOut.push({ location: FieldDataTypes.COORDINATE });
}
// TODO: Make cell connectivity available when field-data is associated with
// cells or graph edges.
return arraysOut;
};

publicAPI.createSimpleFormulaObject = (
locn,
arrNames,
resultName,
singleValueFormula,
options = {}
) => ({
getArrays: (inData) => ({
// don't augment input data array in case of structured input dataset
input: inData[0]?.isA('vtkImageData')
? arrNames.map((x) => ({ location: locn, name: x }))
: publicAPI.augmentInputArrays(
locn,
arrNames.map((x) => ({ location: locn, name: x }))
),
output: [
{
location: locn,
name: resultName,
attribute:
'outputAttributeType' in options
? options.outputAttributeType
: AttributeTypes.SCALARS,
numberOfComponents:
'numberOfOutputComponents' in options
? options.numberOfOutputComponents
: 1,
},
],
}),
evaluate: (arraysIn, arraysOut) => {
const tuples = new Array(arraysIn.length);
const arrayInAccessors = arraysIn.map((x, jj) => {
const nc = x.getNumberOfComponents();
const rawData = x.getData();
return nc === 1
? (ii) => rawData[ii]
: (ii) => x.getTuple(ii, tuples[jj]);
});
const arrayOut = arraysOut[0];
const arrayOutRaw = arrayOut.getData();
const nc = arrayOut.getNumberOfComponents();
let tupleOut = new Array(nc);
if (nc === 1) {
arrayOutRaw.forEach((xxx, ii) => {
arrayOutRaw[ii] = singleValueFormula(
...arrayInAccessors.map((x) => x(ii)),
ii,
tupleOut
);
});
} else {
const nt = arrayOut.getNumberOfTuples();
for (let ii = 0; ii < nt; ++ii) {
tupleOut = singleValueFormula(
...arrayInAccessors.map((x) => x(ii)),
ii,
tupleOut
);
arrayOut.setTuple(ii, tupleOut);
}
}
},
});

publicAPI.setFormulaSimple = (
locn,
arrNames,
resultName,
formula,
options = {}
) =>
publicAPI.setFormula(
publicAPI.createSimpleFormulaObject(
locn,
arrNames,
resultName,
formula,
options
)
);

publicAPI.prepareArrays = (arraySpec, inData, outData) => {
const arraysIn = [];
const arraysOut = [];
arraySpec.input.forEach((spec) => {
if (spec.location === FieldDataTypes.COORDINATE) {
arraysIn.push(inData.getPoints());
} else {
const fetchArrayContainer = [
[FieldDataTypes.UNIFORM, (x) => x.getFieldData()],
[FieldDataTypes.POINT, (x) => x.getPointData()],
[FieldDataTypes.CELL, (x) => x.getCellData()],
[FieldDataTypes.VERTEX, (x) => x.getVertexData()],
[FieldDataTypes.EDGE, (x) => x.getEdgeData()],
[FieldDataTypes.ROW, (x) => x.getRowData()],
].reduce((result, value) => {
result[value[0]] = value[1];
return result;
}, {});
const dsa =
'location' in spec && spec.location in fetchArrayContainer
? fetchArrayContainer[spec.location](inData)
: null;
if (dsa) {
if (spec.name) {
arraysIn.push(dsa.getArrayByName(spec.name));
} else if ('index' in spec) {
arraysIn.push(dsa.getArrayByIndex(spec.index));
} else if (
'attribute' in spec &&
spec.location !== FieldDataTypes.UNIFORM
) {
arraysIn.push(dsa.getActiveAttribute(spec.attribute));
} else {
vtkWarningMacro(
`No matching array for specifier "${JSON.stringify(spec)}".`
);
arraysIn.push(null);
}
} else {
vtkWarningMacro(
`Specifier "${JSON.stringify(
spec
)}" did not provide a usable location.`
);
arraysIn.push(null);
}
}
});
arraySpec.output.forEach((spec) => {
const fullSpec = { ...spec };
const ncomp =
'numberOfComponents' in fullSpec ? fullSpec.numberOfComponents : 1;
if (spec.location === FieldDataTypes.UNIFORM && 'tuples' in fullSpec) {
fullSpec.size = ncomp * fullSpec.tuples;
}
if (spec.location === FieldDataTypes.COORDINATE) {
const inPts = inData.getPoints();
const pts = vtkPoints.newInstance({ dataType: inPts.getDataType() });
pts.setNumberOfPoints(
inPts.getNumberOfPoints(),
inPts.getNumberOfComponents()
);
outData.setPoints(pts);
arraysOut.push(pts);
} else {
const fetchArrayContainer = [
[
FieldDataTypes.UNIFORM,
(x) => x.getFieldData(),
(x, y) => ('tuples' in y ? y.tuples : 0),
],
[
FieldDataTypes.POINT,
(x) => x.getPointData(),
(x) => x.getNumberOfPoints(),
],
[
FieldDataTypes.CELL,
(x) => x.getCellData(),
(x) => x.getNumberOfCells(),
],
[
FieldDataTypes.VERTEX,
(x) => x.getVertexData(),
(x) => x.getNumberOfVertices(),
],
[
FieldDataTypes.EDGE,
(x) => x.getEdgeData(),
(x) => x.getNumberOfEdges(),
],
[
FieldDataTypes.ROW,
(x) => x.getRowData(),
(x) => x.getNumberOfRows(),
],
].reduce((result, value) => {
result[value[0]] = { getData: value[1], getSize: value[2] };
return result;
}, {});
let dsa = null;
let tuples = 0;
if ('location' in spec && spec.location in fetchArrayContainer) {
dsa = fetchArrayContainer[spec.location].getData(outData);
tuples = fetchArrayContainer[spec.location].getSize(inData, fullSpec);
}
if (tuples <= 0) {
vtkWarningMacro(
`Output array size could not be determined for ${JSON.stringify(
spec
)}.`
);
arraysOut.push(null);
} else if (dsa) {
fullSpec.size = ncomp * tuples;
const arrOut = vtkDataArray.newInstance(fullSpec);
const arrIdx = dsa.addArray(arrOut);
if (
'attribute' in fullSpec &&
spec.location !== FieldDataTypes.UNIFORM
) {
dsa.setActiveAttributeByIndex(arrIdx, fullSpec.attribute);
}
arraysOut.push(arrOut);
} else {
vtkWarningMacro(
`Specifier "${JSON.stringify(
spec
)}" did not provide a usable location.`
);
arraysOut.push(null);
}
}
});
return { arraysIn, arraysOut };
};

publicAPI.requestData = (inData, outData) => {
if (!model.formula) {
return 0;
}

const arraySpec = model.formula.getArrays(inData);

const newDataSet = vtk({ vtkClass: inData[0].getClassName() });
newDataSet.shallowCopy(inData[0]);
outData[0] = newDataSet;

const arrays = publicAPI.prepareArrays(arraySpec, inData[0], outData[0]);
model.formula.evaluate(arrays.arraysIn, arrays.arraysOut);

return 1;
};
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

const DEFAULT_VALUES = {
formula: {
getArrays: () => ({ input: [], output: [] }),
evaluate: () => null,
},
};

// ----------------------------------------------------------------------------

export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);

// Make this a VTK object
macro.obj(publicAPI, model);

// Also make it an algorithm with one input and one output
macro.algo(publicAPI, model, 1, 1);

// Object specific methods
vtkCalculator(publicAPI, model);
}

// ----------------------------------------------------------------------------

export const newInstance = macro.newInstance(extend, 'vtkCalculator');

// ----------------------------------------------------------------------------

export default { newInstance, extend };