Skip to content

Add GPU-aware functionObjects: FunctionObjectIO, Forces, ForceCoeffs#265

Open
HendriceH wants to merge 13 commits into
feat/turbModelfrom
feat/forceCoeffs
Open

Add GPU-aware functionObjects: FunctionObjectIO, Forces, ForceCoeffs#265
HendriceH wants to merge 13 commits into
feat/turbModelfrom
feat/forceCoeffs

Conversation

@HendriceH
Copy link
Copy Markdown
Collaborator

@HendriceH HendriceH commented Mar 26, 2026

Add GPU-aware neoForces and neoForceCoeffs function objects

Summary

This PR introduces the first NeoFOAM function objects: neoForces and neoForceCoeffs. Both are registered in OpenFOAM's runtime selection table and can be dropped into any controlDict functions block alongside standard OpenFOAM function objects. Their output files are format-compatible with OpenFOAM's built-in forces and forceCoeffs, so existing post-processing scripts work unmodified.


Architecture

FunctionObjectIO — base class for all NeoFOAM function objects

FunctionObjectIO (include/NeoFOAM/functionObjects/functionObjectIO.hpp) inherits Foam::functionObject and provides:

  • Output directory managementpostProcessing/<name>/<startTime>/, created on first write.
  • File lifetime managementgetOrCreateFile(filename, headerCallback) opens the file once, calls a user-supplied std::function<void(std::ostream&)> to write the multi-line header on creation, and returns the persistent std::ofstream& for all subsequent appends. No header-written flags needed by callers.
  • OpenFOAM-compatible formatting helpers that mirror Foam::functionObjects::writeFile:
    • writeCurrentTime, writeHeader, writeHeaderValue, writeCommented, writeTabbed, writeVec3, fmtScalar, fmtVec3
    • Constants: writePrecision = 6, charWidth = 14 — scientific notation, fixed field width throughout.

Pattern for future function objects: subclass FunctionObjectIO, implement execute() for GPU work and write() for I/O. Call getOrCreateFile with a lambda that writes the header once; use the formatting helpers for consistent output.

DatabaseWrapper — bridging NeoN::Database and Foam::Time

NeoN::Database lives inside NeoFOAM::RunTime and is not directly reachable from within a Foam::functionObject, which only has access to const Foam::Time&. DatabaseWrapper (include/NeoFOAM/datastructures/databaseWrapper.hpp) solves this by subclassing Foam::regIOobject, registering itself in Foam::Time's object registry at RunTime construction, and storing a non-owning pointer to NeoN::Database. Function objects look it up via time_.lookupObject<DatabaseWrapper>(DatabaseWrapper::registryName).

Lifetime is safe: RunTime owns DatabaseWrapper via std::unique_ptr; LIFO destruction ensures DatabaseWrapper unregisters from the Foam::Time registry before Foam::Time itself is destroyed.

Pattern for future function objects: any function object that needs to reach the NeoN database follows the same const-safe lookup — time_.foundObject<DatabaseWrapper>(...) + time_.lookupObject<DatabaseWrapper>(...).db().


neoForces

Registered as type neoForces in controlDict.

execute() — GPU pressure force/moment integral:

  1. Looks up NeoN::Database via DatabaseWrapper in the Foam::Time registry.
  2. Finds the named pressure field in fvcc::VectorCollection — the field registered there by the solver (e.g. neoIcoFoam via constructAndRegister). No field copy from OpenFOAM occurs here.
  3. Runs a NeoN::parallelFor kernel over boundary patch faces, accumulating F = ρ(p − p_ref)·Sf and the corresponding moment into a 6-element device buffer via atomic_add.
  4. Copies exactly 6 scalars (96 bytes) to host — independent of mesh size.

write() — two files matching OpenFOAM format exactly:

force.dat:

# Force
# CofR          : (x y z)
#
# Time    total_x    total_y    total_z    pressure_x    pressure_y    pressure_z    viscous_x    viscous_y    viscous_z

moment.dat — identical structure for moment components.


neoForceCoeffs

Registered as type neoForceCoeffs in controlDict. Extends neoForces.

New dict entries: dragDir (default (1 0 0)) and liftDir (default (0 0 1)). sideDir is derived as liftDir × dragDir (right-hand system), mirroring OpenFOAM's coordinate system convention.

execute() — after calling Forces::execute(), projects the global force/moment vectors onto the three direction axes via dot product and normalises:

Cd = F · dragDir / (½ρU²Aref)          CmRoll  = M · dragDir  / (½ρU²Aref·lRef)
Cl = F · liftDir / (½ρU²Aref)          CmPitch = M · sideDir  / (½ρU²Aref·lRef)
Cs = F · sideDir / (½ρU²Aref)          CmYaw   = M · liftDir  / (½ρU²Aref·lRef)

Front/rear axle split (matches OpenFOAM exactly):

Cd(f) = 0.5·Cd + CmRoll     Cd(r) = 0.5·Cd − CmRoll
Cl(f) = 0.5·Cl + CmPitch    Cl(r) = 0.5·Cl − CmPitch
Cs(f) = 0.5·Cs + CmYaw      Cs(r) = 0.5·Cs − CmYaw

write() — writes coefficient.dat only (force/moment files are the responsibility of neoForces alone):

# Force and moment coefficients
# dragDir    : (...)
# sideDir    : (...)
# liftDir    : (...)
# rollAxis   : (...)
# pitchAxis  : (...)
# yawAxis    : (...)
# magUInf    : ...
# lRef       : ...
# Aref       : ...
# CofR       : (...)
#
# Time    Cd    Cd(f)    Cd(r)    Cl    Cl(f)    Cl(r)    CmPitch    CmRoll    CmYaw    Cs    Cs(f)    Cs(r)

12 scalar columns in alphabetical order, matching OpenFOAM's sorted coefficient map.


Tests

test/test_forceCoeffs.cpp verifies neoForces against a pure OpenFOAM reference implementation (no NeoN) for:

  • Uniform pressure on a single patch
  • Random pressure field on a single patch
  • Random pressure field summed over multiple patches

The NeoN GPU result must match the OpenFOAM scalar reduction to within 1e-10.


Files changed

File Description
include/NeoFOAM/functionObjects/functionObjectIO.hpp / .cpp Base class: file I/O, formatting helpers, header callback
include/NeoFOAM/functionObjects/forces.hpp / .cpp GPU force integral; force.dat + moment.dat
include/NeoFOAM/functionObjects/forceCoeffs.hpp / .cpp Direction-vector projection, front/rear split, coefficient.dat
include/NeoFOAM/datastructures/databaseWrapper.hpp / .cpp Foam::regIOobject wrapper exposing NeoN::Database to function objects
include/NeoFOAM/datastructures/runTime.hpp Added std::unique_ptr<DatabaseWrapper> dbWrapper
src/auxiliary/setup.cpp Constructs and registers DatabaseWrapper in createAdapterRunTime
test/test_forceCoeffs.cpp Integration tests

@github-actions
Copy link
Copy Markdown

Deployed test documentation to https://exasim-project.com/NeoFOAM/Build_PR_265

@HendriceH HendriceH self-assigned this Apr 15, 2026
@HendriceH HendriceH marked this pull request as ready for review April 15, 2026 08:59
@HendriceH HendriceH requested a review from greole April 15, 2026 09:00
Copy link
Copy Markdown
Contributor

@greole greole left a comment

Choose a reason for hiding this comment

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

Some quick, comments, please make sure that the clang-tidy check also passes. And use scalar instead of double in case we need to run on SP hardware.

Comment thread src/functionObjects/forceCoeffs.cpp Outdated
Comment thread src/functionObjects/forceCoeffs.cpp Outdated
Comment thread include/NeoFOAM/functionObjects/forceCoeffs.hpp Outdated
Comment thread include/NeoFOAM/functionObjects/forceCoeffs.hpp Outdated
Comment thread include/NeoFOAM/functionObjects/forceCoeffs.hpp Outdated
Comment thread include/NeoFOAM/functionObjects/forces.hpp Outdated
Comment on lines +151 to +156
NeoN::atomic_add(&accView[0], fp[0]);
NeoN::atomic_add(&accView[1], fp[1]);
NeoN::atomic_add(&accView[2], fp[2]);
NeoN::atomic_add(&accView[3], mp[0]);
NeoN::atomic_add(&accView[4], mp[1]);
NeoN::atomic_add(&accView[5], mp[2]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This might be not ideal to have a bunch of atomics here. If this is a bottleneck, which i don't know, it migth make sense to split this loop into a map and a parallel reduce.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've run in for the 6.3M cells WindsorBody case and didn't see any performance impact. Should test for bigger cases.

<< "Available patches: " << pbm.names()
<< Foam::abort(Foam::FatalError);
}
patchIndices_.push_back(static_cast<int>(idx));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this loop only part of initializiation or called multiple times?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The loop should not executed one meshAdapter_ is set. So only in initialisation.

Copy link
Copy Markdown

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 the first NeoFOAM GPU-aware OpenFOAM functionObject implementations (neoForces, neoForceCoeffs) plus shared I/O utilities and a DatabaseWrapper bridge so function objects can access the NeoN database via Foam::Time’s object registry.

Changes:

  • Introduce FunctionObjectIO base class for OpenFOAM-compatible postProcessing output management and formatting.
  • Add GPU pressure force/moment integration (neoForces) and coefficient projection/normalisation (neoForceCoeffs) with RT selection table registration.
  • Add DatabaseWrapper registration in createAdapterRunTime and new tests wired into the test suite.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
include/NeoFOAM/functionObjects/functionObjectIO.hpp Declares base class for persistent output streams and OpenFOAM-style formatting helpers.
src/functionObjects/functionObjectIO.cpp Implements output directory selection, file creation/header-once, and formatting helpers.
include/NeoFOAM/functionObjects/forces.hpp Declares neoForces API and documents intended behavior/output.
src/functionObjects/forces.cpp Implements RTST registration, mesh adapter lookup, GPU patch reduction, and writes force.dat/moment.dat.
include/NeoFOAM/functionObjects/forceCoeffs.hpp Declares neoForceCoeffs (extends Forces) and documents coefficient output.
src/functionObjects/forceCoeffs.cpp Implements direction handling, coefficient computation, and writes coefficient.dat.
include/NeoFOAM/datastructures/databaseWrapper.hpp Declares DatabaseWrapper for exposing NeoN::Database through Foam::Time registry.
src/datastructures/databaseWrapper.cpp Implements wrapper registration key and constructor wiring.
include/NeoFOAM/datastructures/runTime.hpp Extends RunTime to own the DatabaseWrapper for safe lifetime/registry ordering.
src/auxiliary/setup.cpp Registers DatabaseWrapper during createAdapterRunTime and adjusts mesh registry handling.
src/CMakeLists.txt Adds new functionObject and database wrapper sources to the library build.
test/test_forceCoeffs.cpp Adds integration tests comparing NeoN GPU forces to OpenFOAM reference arithmetic.
test/CMakeLists.txt Registers the new forceCoeffs unit test target.
CHANGELOG.md Notes the addition of forceCoeffs/functionObject infrastructure in the unreleased changelog.

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

Comment thread src/functionObjects/forces.cpp Outdated
Comment thread include/NeoFOAM/functionObjects/forces.hpp Outdated
Comment thread include/NeoFOAM/functionObjects/forces.hpp Outdated
Comment thread src/functionObjects/forceCoeffs.cpp Outdated
Comment on lines +41 to +45
Foam::vector dragFoam = dict.getOrDefault<Foam::vector>("dragDir", Foam::vector(1, 0, 0));
Foam::vector liftFoam = dict.getOrDefault<Foam::vector>("liftDir", Foam::vector(0, 0, 1));
dragDir_ = NeoN::Vec3(dragFoam[0], dragFoam[1], dragFoam[2]);
liftDir_ = NeoN::Vec3(liftFoam[0], liftFoam[1], liftFoam[2]);

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

dragDir_/liftDir_ are treated as direction vectors but are not normalised. This makes coefficient magnitudes depend on the input vector lengths, and can also produce a non-unit sideDir_ from the cross product below. Consider normalising/validating these vectors (non-zero, non-parallel) and normalising sideDir_.

Copilot uses AI. Check for mistakes.
Comment thread src/functionObjects/forceCoeffs.cpp
Comment thread CHANGELOG.md Outdated
Comment thread test/test_forceCoeffs.cpp Outdated
Comment thread test/test_forceCoeffs.cpp Outdated
Comment thread include/NeoFOAM/functionObjects/forceCoeffs.hpp
HendriceH and others added 11 commits April 20, 2026 18:55
Introduces a base I/O class and two concrete GPU-accelerated function
objects that integrate with OpenFOAM's controlDict RTST pattern:

- FunctionObjectIO: abstract base inheriting Foam::functionObject,
  providing persistent postProcessing/<name>/<startTime>/ file I/O
- Forces (neoForces): GPU kernel via parallelFor + atomic_add into a
  6-element device buffer; only 96 bytes transferred to host per patch
- ForceCoeffs (neoForceCoeffs): extends Forces with dynamic pressure
  normalisation (Cd = F / (0.5*rho*U²*Aref))

Also fixes createAdapterRunTime to check out any pre-registered plain
fvMesh before creating the MeshAdapter, ensuring the adapter is
findable via lookupObject in tests and in production solvers.

Integration test (test_forceCoeffs) verifies GPU results match
OpenFOAM reference for uniform pressure, random pressure, multiple
patches, and coefficient normalisation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@HendriceH HendriceH changed the base branch from develop to feat/turbModel April 20, 2026 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants