Skip to content

add flow computation tools#237

Closed
jeffamstutz wants to merge 12 commits intonext_releasefrom
aparis/lambda2
Closed

add flow computation tools#237
jeffamstutz wants to merge 12 commits intonext_releasefrom
aparis/lambda2

Conversation

@jeffamstutz
Copy link
Collaborator

No description provided.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a “Flow Analysis” tool to the ImGui UI and introduces procedural computation of common vortical quantities (vorticity magnitude, helicity, Lambda2, Q-criterion) from three scalar velocity component fields, alongside importer updates to expose vector component fields as separate SpatialFields.

Changes:

  • Add a new ImGui modal (Flow Analysis) to select U/V/W SpatialFields and compute derived flow volumes.
  • Add tsd::io::computeVorticity() (structured + NanoVDB paths, plus an optional VTK unstructured path) and a new core vort implementation.
  • Update VTI/VTU importers to expose 3-component point arrays as separate component fields (_x/_y/_z), enabling selection for flow analysis.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tsd/src/tsd/ui/imgui/modals/VorticityDialog.h Declares the new Flow Analysis modal and its UI state.
tsd/src/tsd/ui/imgui/modals/VorticityDialog.cpp Implements field selection UI and kicks off background computation via task modal.
tsd/src/tsd/ui/imgui/CMakeLists.txt Builds the new modal implementation.
tsd/src/tsd/ui/imgui/Application.h Wires the new modal into the app.
tsd/src/tsd/ui/imgui/Application.cpp Adds menu entry and renders the modal when visible.
tsd/src/tsd/io/procedural/computeVorticity.hpp Declares the new procedural API and options/result types.
tsd/src/tsd/io/procedural/computeVorticity.cpp Implements extraction + computation + volume wrapping for outputs.
tsd/src/tsd/io/procedural.hpp Exposes computeVorticity in the procedural umbrella header.
tsd/src/tsd/io/importers/import_VTU.cpp Exposes all point arrays as SpatialFields sharing topology; adds default volume colormap.
tsd/src/tsd/io/importers/import_VTI.cpp Splits 3-component point arrays into _x/_y/_z fields (main field becomes _x).
tsd/src/tsd/io/importers/import_USD.cpp Formatting-only include/order and wrapping changes.
tsd/src/tsd/io/CMakeLists.txt Builds the new procedural implementation file.
tsd/src/tsd/core/algorithms/vort.h Adds the core numerical implementation for vortical quantities.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +34 to +37
foreach_item_const(scene.objectDB().field, [&](const SpatialField *sf) {
if (sf) {
fields.push_back(const_cast<SpatialField *>(sf));
fieldNames.push_back(sf->name());
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

computeVorticity() only reads the input SpatialFields, but the UI has to const_cast SpatialField* out of a const iteration to call it. Consider changing computeVorticity() (and helper extract* functions) to take const SpatialField* for u/v/w so callers don’t need to cast away constness.

Copilot uses AI. Check for mistakes.
Comment on lines +267 to +270
len);

std::cout << "[vort] Vortical variables computed" << std::endl;
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prints to stdout every time vort() runs (and vort.h includes just for that). In production this will spam logs and can significantly slow large computations. Please remove the std::cout line (or switch to the project logging utilities behind an explicit verbose/debug flag).

Copilot uses AI. Check for mistakes.
Comment on lines +215 to +227
inline void vort(const float *u,
const float *v,
const float *w,
const double *x_,
const double *y_,
const double *z_,
float *vorticity,
float *helicity,
float *lambda2,
float *qCriterion,
size_t nx,
size_t ny,
size_t nz)
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New numerical routines (grad3D/vort/vort_from_jacobians) are added without any unit tests. Since tsd/tests already uses Catch2 for core utilities, please add a small test that checks vorticity/lambda2/Q outputs on a simple analytic velocity field (e.g., solid-body rotation) and verifies boundary handling on a minimal grid.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to 137
// --- Write point data arrays ---
vtkPointData *pointData = grid->GetPointData();
for (uint32_t i = 0; i < pointData->GetNumberOfArrays(); ++i) {
vtkDataArray *array = pointData->GetArray(i);

int numComponents = array->GetNumberOfComponents();
if (numComponents > 1) {

if (numComponents == 1) {
auto a = makeArray3DFromVTK(
scene, array, dims[0], dims[1], dims[2], "[import_VTI]");
field->setParameterObject("data", *a);
break;
} else if (numComponents == 3) {
// Split into 3 scalar SpatialFields: {name}_x, {name}_y, {name}_z
const char *arrName = array->GetName();
std::string baseName = (arrName && arrName[0] != '\0')
? std::string(arrName)
: fileOf(filepath);

std::string nameX = baseName + "_x";
std::string nameY = baseName + "_y";
std::string nameZ = baseName + "_z";

size_t n = (size_t)dims[0] * (size_t)dims[1] * (size_t)dims[2];

auto arrX = scene.createArray(ANARI_FLOAT32, dims[0], dims[1], dims[2]);
auto arrY = scene.createArray(ANARI_FLOAT32, dims[0], dims[1], dims[2]);
auto arrZ = scene.createArray(ANARI_FLOAT32, dims[0], dims[1], dims[2]);

float *pX = arrX->mapAs<float>();
float *pY = arrY->mapAs<float>();
float *pZ = arrZ->mapAs<float>();

for (vtkIdType idx = 0; idx < (vtkIdType)n; ++idx) {
double tuple[3] = {0.0, 0.0, 0.0};
array->GetTuple(idx, tuple);
pX[idx] = (float)tuple[0];
pY[idx] = (float)tuple[1];
pZ[idx] = (float)tuple[2];
}

arrX->unmap();
arrY->unmap();
arrZ->unmap();

// Main field (_x component)
field->setName(nameX.c_str());
field->setParameterObject("data", *arrX);

// _y component field in the scene object pool
auto fieldY = scene.createObject<SpatialField>(
tokens::spatial_field::structuredRegular);
fieldY->setName(nameY.c_str());
fieldY->setParameter("origin", float3(origin[0], origin[1], origin[2]));
fieldY->setParameter(
"spacing", float3(spacing[0], spacing[1], spacing[2]));
fieldY->setParameterObject("data", *arrY);

// _z component field in the scene object pool
auto fieldZ = scene.createObject<SpatialField>(
tokens::spatial_field::structuredRegular);
fieldZ->setName(nameZ.c_str());
fieldZ->setParameter("origin", float3(origin[0], origin[1], origin[2]));
fieldZ->setParameter(
"spacing", float3(spacing[0], spacing[1], spacing[2]));
fieldZ->setParameterObject("data", *arrZ);

logStatus(
"[import_VTI] split 3-component array '%s' into '%s', '%s', '%s'",
baseName.c_str(),
nameX.c_str(),
nameY.c_str(),
nameZ.c_str());
break;
} else {
logWarning(
"[import_VTI] only single-component arrays are supported, "
"array '%s' has %d components -- only using first component",
"[import_VTI] array '%s' has %d components (only 1 or 3 are "
"supported) -- skipping",
array->GetName(),
numComponents);
continue;
}

auto a = makeArray3DFromVTK(
scene, array, dims[0], dims[1], dims[2], "[import_VTI]");
field->setParameterObject("data", *a);
break;
}

return field;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the VTI contains only arrays with component counts other than 1 or 3, this loop will skip everything and the function will still return a SpatialField with its default (invalid) 'data' parameter. That can break rendering/processing later. Please detect the "no supported arrays found" case and return {} (and/or remove the created field) with a clear error message.

Copilot uses AI. Check for mistakes.
Comment on lines +332 to +401
// Helper: create a new unstructured SpatialField with shared topology
auto makeTopoField = [&](const std::string &name) -> SpatialFieldRef {
auto f = scene.createObject<tsd::core::SpatialField>(
tokens::spatial_field::unstructured);
f->setName(name.c_str());
f->setParameterObject("vertex.position", *vertexArray);
f->setParameterObject("index", *indexArray);
f->setParameterObject("cell.index", *cellIndexArray);
f->setParameterObject("cell.type", *cellTypesArray);
return f;
};

// Vertex data: expose all point arrays
vtkPointData *pointData = grid->GetPointData();
for (int i = 0; i < std::min(1, pointData->GetNumberOfArrays()); ++i) {
SpatialFieldRef firstField;
std::string baseName = fileOf(filepath);

for (int i = 0; i < pointData->GetNumberOfArrays(); ++i) {
vtkDataArray *array = pointData->GetArray(i);
if (!array)
continue;
auto a = makeFloatArray1D(scene, array, numPoints);
field->setParameterObject("vertex.data", *a);
int nComp = array->GetNumberOfComponents();
std::string arrName = (array->GetName() && array->GetName()[0] != '\0')
? array->GetName()
: baseName;

if (nComp == 1) {
auto dataArr = scene.createArray(ANARI_FLOAT32, numPoints);
auto *buf = dataArr->mapAs<float>();
for (vtkIdType j = 0; j < numPoints; ++j)
buf[j] = static_cast<float>(array->GetComponent(j, 0));
dataArr->unmap();
auto f = makeTopoField(arrName);
f->setParameterObject("vertex.data", *dataArr);
if (!firstField)
firstField = f;
} else if (nComp == 3) {
for (int c = 0; c < 3; ++c) {
const char *suffix = (c == 0) ? "_x" : (c == 1) ? "_y" : "_z";
std::string compName = arrName + suffix;
auto dataArr = scene.createArray(ANARI_FLOAT32, numPoints);
auto *buf = dataArr->mapAs<float>();
for (vtkIdType j = 0; j < numPoints; ++j)
buf[j] = static_cast<float>(array->GetComponent(j, c));
dataArr->unmap();
auto f = makeTopoField(compName);
f->setParameterObject("vertex.data", *dataArr);
if (!firstField)
firstField = f;
}
logStatus(
"[import_VTU] split 3-component array '%s' into '%s_x', '%s_y', "
"'%s_z'",
arrName.c_str(),
arrName.c_str(),
arrName.c_str(),
arrName.c_str());
} else {
logWarning(
"[import_VTU] array '%s' has %d components (only 1 or 3 are "
"supported) -- skipping",
arrName.c_str(),
nComp);
}
}

// Cell data
// Fallback: no point arrays → create a topology-only field
if (!firstField)
firstField = makeTopoField(baseName);

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional SpatialFields created for each point-data array are not referenced by any layer node or object parameter, so Scene::removeUnusedObjects()/cleanup will delete them immediately. If these fields are intended to be selectable later (e.g., for Flow Analysis), they need some kind of persistent reference (e.g., insert layer nodes for them under the import root, or attach them to a container object).

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +127
// Main field (_x component)
field->setName(nameX.c_str());
field->setParameterObject("data", *arrX);

// _y component field in the scene object pool
auto fieldY = scene.createObject<SpatialField>(
tokens::spatial_field::structuredRegular);
fieldY->setName(nameY.c_str());
fieldY->setParameter("origin", float3(origin[0], origin[1], origin[2]));
fieldY->setParameter(
"spacing", float3(spacing[0], spacing[1], spacing[2]));
fieldY->setParameterObject("data", *arrY);

// _z component field in the scene object pool
auto fieldZ = scene.createObject<SpatialField>(
tokens::spatial_field::structuredRegular);
fieldZ->setName(nameZ.c_str());
fieldZ->setParameter("origin", float3(origin[0], origin[1], origin[2]));
fieldZ->setParameter(
"spacing", float3(spacing[0], spacing[1], spacing[2]));
fieldZ->setParameterObject("data", *arrZ);

logStatus(
"[import_VTI] split 3-component array '%s' into '%s', '%s', '%s'",
baseName.c_str(),
nameX.c_str(),
nameY.c_str(),
nameZ.c_str());
break;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to VTU: the split _y/_z SpatialFields are created but never referenced, so "Cleanup Unused Objects" will remove them. If the goal is to expose vector components as standalone fields for downstream tools, consider adding a persistent reference (e.g., insert nodes for them, or otherwise keep them alive).

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +82
dst.resize(n);
const float *src = ca->dataAs<float>();
for (size_t i = 0; i < n; ++i)
dst[i] = src[i];
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readCoordsParam() assumes the coords Array is float32 and has at least n elements (dataAs() + unchecked loop). This will assert/UB for ANARI_FLOAT64 coord arrays and can read out of bounds if the array is shorter than the field dimension. Please validate elementType (support float32/float64 like export_StructuredVolumeToNanoVDB does) and verify ca->size() >= n before copying.

Suggested change
dst.resize(n);
const float *src = ca->dataAs<float>();
for (size_t i = 0; i < n; ++i)
dst[i] = src[i];
// Validate element type: support float32 and float64 coordinates
anari::DataType elemType = ca->elementType();
if (elemType != ANARI_FLOAT32 && elemType != ANARI_FLOAT64) {
logError("[computeVorticity] field coord parameter '%s' has unsupported "
"element type (expected float32/float64)",
name);
return false;
}
// Validate we have at least n elements before copying
if (ca->size() < n) {
logError("[computeVorticity] field coord parameter '%s' is too short "
"(have %zu elements, need %zu)",
name,
static_cast<size_t>(ca->size()),
n);
return false;
}
dst.resize(n);
if (elemType == ANARI_FLOAT32) {
const float *src = ca->dataAs<float>();
for (size_t i = 0; i < n; ++i)
dst[i] = static_cast<double>(src[i]);
} else { // ANARI_FLOAT64
const double *src = ca->dataAs<double>();
for (size_t i = 0; i < n; ++i)
dst[i] = src[i];
}

Copilot uses AI. Check for mistakes.
Comment on lines +347 to +353
auto field = scene.createObject<SpatialField>(
tokens::spatial_field::structuredRegular);
field->setName(name.c_str());
field->setParameter("origin", origin);
field->setParameter("spacing", spacing);
field->setParameterObject("data", *dataArr);

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrapAsVolume() always creates a structuredRegular SpatialField and the caller derives a single uniform spacing from the first two coordinates. For structuredRectilinear / nanovdbRectilinear inputs this can misrepresent world-space mapping (non-uniform coords are lost). Either (a) generate structuredRectilinear outputs by preserving coordsX/Y/Z, or (b) explicitly validate the coords are uniformly spaced before converting to structuredRegular.

Copilot uses AI. Check for mistakes.
@jeffamstutz jeffamstutz deleted the aparis/lambda2 branch February 27, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants