Flow / Follow a vector field in three dimensions
Let a three-dimensional vortex change its shape
Project velocity. Evolve its rotational tendency. Watch new modes appear.
Download runnable lesson (.zip)
Includes the model, inputs, required headers and a short run guide.
The two-dimensional exact vortex keeps its shape while viscosity changes its amplitude. This three-dimensional initial field has a genuinely nonlinear evolution on \([0,2\pi)^3\):
\[ \begin{aligned} u&=\sin x\cos y\cos z,\\ v&=-\cos x\sin y\cos z,\\ w&=0. \end{aligned} \]
Taylor and Green’s study provides the historical background for this vortex family (Taylor and Green 1937). Here the retained driver uses a small smoke case to teach OpenPFC’s Fourier vector operations and their diagnostics.
Put the initial field in ordinary lambdas
rx.apply([](double x, double y, double z) {
return std::sin(x) * std::cos(y) * std::cos(z);
});
ry.apply([](double x, double y, double z) {
return -std::cos(x) * std::sin(y) * std::cos(z);
});The third field starts at zero. That is an initial condition, not a constraint maintained throughout the evolution: the nonlinear step can produce a nonzero third velocity component. The retained tests explicitly check that it does.
After transforming all three components, the helper uses the installed projector and state filter:
pfc::field::leray_project(outbox, flow.n, flow.spacing, flow.u.data(),
flow.v.data(), flow.w.data(), nhat);
pfc::field::apply_two_thirds(outbox, flow.n, flow.spacing, flow.u.data(),
flow.v.data(), flow.w.data(), nhat);For a nonzero wave vector, the Leray projection removes the longitudinal component of a vector \(\widehat{\mathbf f}\):
\[ \widehat{P\mathbf f} =\widehat{\mathbf f} -\mathbf k\,\frac{\mathbf k\cdot\widehat{\mathbf f}}{|\mathbf k|^2}. \]
The zero mode is left unchanged. Consistent odd derivative symbols handle Nyquist components; the state-level two-thirds filter also removes the high-frequency part of the initial state and subsequent stages.
Use the installed rotational tendency
With \(\boldsymbol\omega=\nabla\times\mathbf u\), the incompressible velocity equation can be written
\[ \mathbf u_t=P(\mathbf u\times\boldsymbol\omega) +\nu\nabla^2\mathbf u. \]
Projection removes the pressure-gradient contribution. The example composes the public ifrk4_velocity and rotational_tendency operations, with viscosity integrated through precomputed exponentials:
inline void step(Flow &flow) {
auto &fft = flow.stack->fft();
const auto outbox = fft.get_outbox_bounds();
pfc::incompressible::ifrk4_velocity(
outbox, flow.n, flow.spacing, flow.u, flow.v, flow.w, flow.exp_dt,
flow.exp_half, flow.dt,
[&](const std::vector<Complex> &u, const std::vector<Complex> &v,
const std::vector<Complex> &w, std::vector<Complex> &tu,
std::vector<Complex> &tv, std::vector<Complex> &tw) {
pfc::incompressible::rotational_tendency(fft, flow.n, flow.spacing, u, v, w,
tu, tv, tw, true);
},
true);
}
The helper retains the existing one-rank ownership requirement. This chapter does not change its solver or introduce a second velocity projector alongside the installed library implementation.
Define the norms before comparing them
At time zero, trigonometric orthogonality gives
\[ \begin{aligned} E_0&=\tfrac12\langle u^2+v^2+w^2\rangle=\tfrac18,\\ \Omega_0&=\langle|\boldsymbol\omega|^2\rangle=\tfrac34. \end{aligned} \]
This driver’s enstrophy column is the full mean squared vorticity. The two-dimensional vorticity lesson’s \(Z\) contains an additional factor of one half. Definitions, rather than column names alone, determine how to compare those numbers.
The continuous periodic energy identity is
\[ \frac{dE}{dt}=-\nu\Omega. \]
All initial modes have \(|\mathbf k|^2=3\). Pure viscosity acting on that initial shell would give
\[ E_{\mathrm{linear}}(t)=\tfrac18e^{-6\nu t}. \]
The nonlinear tendency transfers energy between shells. Consequently the ke_linear column is an initial-shell comparison, not an exact later-time Navier–Stokes solution. A difference from that curve is not, by itself, a numerical error.
Run the bounded smoke case
From the repository root, with a compatible installed spectral CPU SDK:
mkdir -p results
openpfc compile examples/flow/taylor-green-3d/model.cpp --source-root examples/flow -o results/taylor-green
results/taylor-green --case smoke --outdir results/flow/taylor-greenUse one MPI rank, as required by this driver. The retained smoke case uses \(N=16\), \(\nu=0.05\), \(h=1/64\) and two steps, reaching \(T=1/32\). The model shares verify.hpp; keep their relative layout when using downloaded sources. The CMake target remains taylor_green3d, with its original build location and numerical tests.
Read errors.csv, spectrum.csv and run.json. The smoke case computes diagnostics without a separately refined trajectory, so its solution-error columns are empty. Treat those as unavailable comparisons, rather than zero errors. Inspect divergence, energy, the third velocity component and the shell-energy sum instead. At time zero, energy belongs to the \(|\mathbf k|^2=3\) shell; later rows can show transfer to other shells.
The existing tests verify the analytic initial norms, small divergence, energy loss with positive viscosity, growth of the third component, shell-energy normalization and common-grid restriction. Their criteria remain unchanged. The retained larger spatial and temporal options keep their existing fixed protocols; the smoke command is the small entry point for this lesson.
In your own copy, change the initial amplitude and derive its time-zero energy scaling before running. Then inspect which diagnostics test the initial condition, which test incompressibility and which require a separate grid or time reference. Keeping those questions distinct makes a complex three-dimensional example teachable.