DPM UDF Programming -- Macros, Hooks, Tracked_Particle, and Complete Examples
#+CATEGORY: cfd
#+TAGS: DPM UDF DEFINE_DPM Tracked_Particle Brownian body-force custom-drag custom-BC
#+MATURITY: growing
DPM UDF Programming :: Complete Reference
User-Defined Functions give direct control over every particle in the domain. Fluent ships a fixed set of DPM macros and hooks; each hook fires at a specific point in the tracking loop. This page documents every macro, every hook, the Tracked_Particle structure, compilation versus interpretation, and complete working examples for the most common DPM UDF tasks: custom body force (Brownian), custom drag, custom boundary condition, custom law, and injection initialization.
Prerequisites
- Fluent 2024 R1
- A working DPM case (continuous phase converged)
- C compiler: MSVC (Windows) or gcc (Linux); Fluent ships
compileandinterpretmodes - For compiled UDFs: set up the build environment via = fluent 24.1.0 -shell = or the TUI command = define/user-defined/compiled-functions =
Interpreted vs Compiled
| Mode | When to use | Limitations |
| ------ | ------------- | ------------- |
| Interpreted | Quick tests, simple expressions | No #include, no structs, no pointers, limited C constructs |
| Compiled | All production DPM UDFs | Requires C compiler; full C access |
For DPM, always use compiled mode. Interpreted mode cannot access Tracked_Particle pointers.
The Tracked_Particle Structure
Every DPM parcel is represented by a Tracked_Particle struct, accessed in UDFs via the pointer p. This struct holds everything Fluent knows about the parcel at the current tracking step.
Current Properties (suffixless)
| Macro | Type | Returns |
| ------- | ------ | --------- |
P_DIAM(p) | real | Current diameter [m] |
P_VEL(p) | real[3] | Current velocity vector [m/s] |
P_T(p) | real | Current temperature [K] |
P_MASS(p) | real | Current mass [kg] |
P_RHO(p) | real | Current density [kg/m^3] |
P_TIME(p) | real | Time since injection [s] |
P_LF(p) | real | Liquid fraction [0--1] |
P_VFF(p) | real | Volatile fraction [0--1] |
P_CELL(p) | cell_t | Current cell index |
P_THREAD(p) | Thread * | Current cell thread |
P_FACE(p) | face_t | Current face (if on boundary) |
P_POS(p) | real[3] | Current position vector |
Cell-Entry Properties (suffix 0)
Fluent stores the particle state at the moment it entered the current cell. This is essential for computing time-stepped quantities:
| Macro | Returns |
| ------- | --------- |
P_DIAM0(p) | Diameter at cell entry |
P_VEL0(p) | Velocity at cell entry |
P_T0(p) | Temperature at cell entry |
P_MASS0(p) | Mass at cell entry |
P_TIME0(p) | Time at cell entry |
P_POS0(p) | Position at cell entry |
Injection Properties (prefix P_INIT_)
These are frozen at the moment of injection and never change:
| Macro | Returns |
| ------- | --------- |
P_INIT_DIAM(p) | Diameter at injection |
P_INIT_RHO(p) | Density at injection |
P_INIT_TEMP(p) | Temperature at injection |
P_INIT_LF(p) | Liquid fraction at injection |
P_INIT_MASS(p) | Mass at injection |
P_INIT_TIME(p) | Injection start time (transient) |
Material and Law Properties
| Macro | Returns |
| ------- | --------- |
P_MATERIAL(p) | Pointer to particle material data |
P_CURRENT_LAW(p) | Current DPM law index |
P_NEXT_LAW(p) | Next DPM law index (switch target) |
P_EVAP_SPECIES_INDEX(p) | Evaporation species index |
P_DEVOL_SPECIES_INDEX(p) | Devolatilization species index |
P_OXID_SPECIES_INDEX(p) | Oxidation species index |
P_PROD_SPECIES_INDEX(p) | Product species index |
DPM_EMISSIVITY(p) | Particle emissivity |
DPM_SCATT_FACTOR(p) | Radiation scattering factor |
DPM_LATENT_HEAT(p) | Latent heat [J/kg] |
DPM_SPECIFIC_HEAT(p, t) | Specific heat at temperature t [J/(kg K)] |
DPM_EVAPORATION_TEMPERATURE(p) | Evaporation temperature [K] |
DPM_BOILING_TEMPERATURE(p) | Boiling temperature [K] |
DPM_VOLATILE_FRACTION(p) | Volatile fraction [0--1] |
DPM_CHAR_REACTION(p) | Char fraction flag |
All DPM UDF Hooks
Each hook fires at a specific stage of the tracking loop. The general tracking loop for one parcel is:
DEFINE_DPM_BODY_FORCE -- custom force
Adds a user-defined force to the particle force balance. Fires every sub-step inside every cell the particle traverses.
#include "udf.h"
DEFINE_DPM_BODY_FORCE(brownian_force, p, i)
{
/* i = component index (0=x, 1=y, 2=z) */
/* Return force per unit particle mass [m/s^2] */
real force = 0.0;
/* Example: gravity correction in z-direction */
if (i == 2)
force = -9.81; /* [m/s^2] -- but gravity is already built in */
return force;
}
This is the hook used for Brownian motion -- see cfd_dpm-brownian for the complete implementation.
DEFINE_DPM_DRAG -- custom drag
Override or modify the drag coefficient. The return value is the drag coefficient $C_D$. The fluid Re and particle Re are accessible through the cell thread.
#include "udf.h"
DEFINE_DPM_DRAG(my_non_spherical_drag, p, Re_normal)
{
/* Return drag coefficient for the particle */
real Re_p = Re_normal; /* particle Reynolds number */
real Cd;
if (Re_p < 0.1)
Cd = 24.0 / Re_p; /* Stokes */
else if (Re_p < 1000.0)
Cd = (24.0 / Re_p) * (1.0 + 0.15 * pow(Re_p, 0.687));
else
Cd = 0.44;
return Cd;
}
Activation: In Fluent, Models > Discrete Phase > Interaction > Drag Law > select user-defined and hook my_non_spherical_drag.
DEFINE_DPM_BC -- custom boundary condition
Fires when a particle hits a boundary face. Return values control the particle fate:
| Return value | Meaning |
| -------------- | --------- |
PATH_ACTIVE | Continue tracking |
PATH_ABORT | Kill the particle (absorbed) |
PATH_ESCAPE | Mark as escaped |
#include "udf.h"
DEFINE_DPM_BC(sticking_bc, p, f, f_normal, dim)
{
/* f = boundary face, f_normal = outward normal */
/* Check particle velocity component normal to wall */
real vn = P_VEL(p)[0]*f_normal[0]
+ P_VEL(p)[1]*f_normal[1]
+ P_VEL(p)[2]*f_normal[2];
/* If particle hits wall with low kinetic energy, trap it */
real ke = 0.5 * P_MASS(p) *
(P_VEL(p)[0]*P_VEL(p)[0] +
P_VEL(p)[1]*P_VEL(p)[1] +
P_VEL(p)[2]*P_VEL(p)[2]);
if (ke < 1e-10) /* threshold: effectively at rest */
return PATH_ABORT; /* particle sticks */
else
return PATH_ACTIVE; /* keep tracking (specify rebound below) */
}
DEFINE_DPM_EROSION_RATE -- custom erosion model
Fires on each wall impact. Returns erosion rate [kg/(m^2 s)] or a custom quantity.
#include "udf.h"
DEFINE_DPM_EROSION_RATE(my_erosion, p, f, f_normal, dim)
{
real alpha; /* impact angle */
real v_imp; /* impact velocity magnitude */
real rate;
/* Compute impact angle */
v_imp = sqrt(P_VEL(p)[0]*P_VEL(p)[0] +
P_VEL(p)[1]*P_VEL(p)[1] +
P_VEL(p)[2]*P_VEL(p)[2]);
alpha = acos(
(P_VEL(p)[0]*f_normal[0] +
P_VEL(p)[1]*f_normal[1] +
P_VEL(p)[2]*f_normal[2]) / MAX(v_imp, 1e-12));
/* Finnie model: rate ~ v^2 * f(alpha) */
rate = P_MASS(p) * v_imp * v_imp *
(sin(alpha) - 0.3 * sin(3*alpha));
return MAX(rate, 0.0);
}
DEFINE_DPM_INJECTION_INIT -- per-particle initialization
Fires once per parcel at injection time. Use to set unique initial properties.
#include "udf.h"
DEFINE_DPM_INJECTION_INIT(random_temp_init, I)
{
/* I = pointer to Injection struct */
cell_t c;
Thread *t;
real T_rand;
begin_c_loop(c, t)
{
/* For each parcel in this injection */
int n;
for (n = 0; n < I->nop; n++)
{
/* Random temperature around 350 K +/- 20 K */
T_rand = 350.0 + 20.0 *
((real)rand() / (real)RAND_MAX - 0.5);
P_INIT_TEMP(I->p[n]) = T_rand;
}
}
end_c_loop(c, t)
}
DEFINE_DPM_LAW -- custom physics law
Define entirely new particle physics (e.g., custom evaporation, dissolution, swelling).
#include "udf.h"
DEFINE_DPM_LAW(my_dissolution, p, ci, boiling)
{
/* Dissolve particle at rate proportional to surface area */
real d = P_DIAM(p);
real k_diss = 1e-6; /* dissolution rate constant [m/s] */
real dt = CURRENT_TIMESTEP;
/* d(diameter)/dt = -k_diss */
real d_new = d - k_diss * dt;
if (d_new <= 0.0)
{
/* Particle fully dissolved */
P_DIAM(p) = 0.0;
P_MASS(p) = 0.0;
}
else
{
P_DIAM(p) = d_new;
/* Update mass assuming constant density */
P_MASS(p) = P_RHO(p) * M_PI / 6.0 * pow(d_new, 3.0);
}
}
DEFINE_DPM_SWITCH -- custom law-switching rule
Decides when to switch from current law to next law. Fires before the law is applied.
#include "udf.h"
DEFINE_DPM_SWITCH(my_switch, p, ci)
{
/* Switch from heating (law 0) to evaporation (law 1)
when particle reaches 373 K */
if (P_CURRENT_LAW(p) == 0 && P_T(p) >= 373.0)
{
P_CURRENT_LAW(p) = 1; /* activate evaporation law */
}
}
DEFINE_DPM_SCALAR_UPDATE -- per-cell update
Fires every time a particle enters a new cell. Use to update user-defined memory (UDM) or accumulate statistics.
#include "udf.h"
DEFINE_DPM_SCALAR_UPDATE(track_counter, c, t, initialize, p)
{
/* initialize = TRUE on first cell entry after injection */
if (initialize)
{
/* Reset a counter stored in UDM */
C_STORAGE_R(c, t, SV_DPM_0) = 0.0;
}
/* Accumulate number of cells visited */
C_STORAGE_R(c, t, SV_DPM_0) += 1.0;
}
DEFINE_DPM_SOURCE -- source term control
Fires when computing particle source terms to the continuous phase (mass, momentum, energy). Gives full control over how the source is distributed.
#include "udf.h"
DEFINE_DPM_SOURCE(my_source, c, t, s, p, strength, dim)
{
/* s = DPM_Source_Array: momentum, energy, mass sources */
/* Modify source terms before they are deposited */
/* Example: scale momentum source by a factor */
real factor = 1.2;
s->momentum[0] *= factor;
s->momentum[1] *= factor;
s->momentum[2] *= factor;
}
Compilation and Hooking
Compile workflow
1. Write the UDF .c file 2. In Fluent: = define/user-defined/compiled-functions = 3. Click Build then Load 4. Hook the function: Models > Discrete Phase > Interaction, select the relevant UDF from dropdown
Common compilation errors
| Error | Cause | Fix |
| ------- | ------- | ----- |
error C2065: 'P_DIAM': undeclared identifier | Missing #include "udf.h" | Add the include |
error LNK2019: unresolved external symbol | Function name mismatch between DEFINE_* and the hook name | Check spelling matches exactly |
| =warning C4244: '=' conversion from 'double' to 'real'= | Fluent uses real (double or float depending on precision) | Cast explicitly: (real)value |
error: cannot open include file 'surf.h' | Using internal headers not available to UDFs | Restrict to public macros listed in the UDF manual |
Single vs Double precision
Fluent real is float in single precision, double in double precision. Always use real in UDFs, never hardcode float or double.
User-Defined Memory (UDM)
UDM stores per-cell or per-face data that persists across iterations.
Allocating UDM
TUI: = define/user-defined/memory =
Enter the number of cell memory locations and face memory locations needed.
Accessing UDM
| Context | Cell UDM | Face UDM |
| --------- | ---------- | ---------- |
| Read | C_STORAGE_R(c, t, n) | F_STORAGE_R(f, t, n) |
| Write | =C_STORAGE_R(c, t, n) = value= | =F_STORAGE_R(f, t, n) = value= |
n is 0-indexed. If you allocated 3 cell UDMs, valid indices are 0, 1, 2.
Using UDM with DEFINE_DPM_SCALAR_UPDATE
DEFINE_DPM_SCALAR_UPDATE(particle_count_udm, c, t, init, p)
{
if (init)
C_STORAGE_R(c, t, SV_DPM_0) = 0.0;
C_STORAGE_R(c, t, SV_DPM_0) += 1.0;
}
After the DPM iteration, visualize User Defined Memory 0 as a contour to see which cells received the most particle traffic.
Complete Brownian Force UDF for PM2.5
This is the complete Brownian force UDF for the shisha PM2.5 simulation. See cfd_dpm-brownian for full theory.
#include "udf.h"
#include "dpm.h"
#include <math.h>
/* Constants */
#define KB 1.38065e-23 /* Boltzmann constant [J/K] */
#define PI 3.14159265359
DEFINE_DPM_BODY_FORCE(brownian_force_pm25, p, i)
{
/*
* Brownian force model:
* F_B = xi * sqrt(2 * kB * T * 3 * pi * mu * d * dt) / dt
*
* where:
* xi = standard normal random number
* kB = Boltzmann constant
* T = local fluid temperature
* mu = local fluid dynamic viscosity
* d = particle diameter
* dt = particle timestep
*/
Thread *t = P_CELL_THREAD(p);
cell_t c = P_CELL(p);
real T = C_T(c, t); /* fluid temperature [K] */
real mu = C_MU_EFF(c, t); /* effective viscosity [Pa s] */
real d = P_DIAM(p); /* particle diameter [m] */
/* Particle timestep */
real dt = CURRENT_TIMESTEP;
if (dt <= 0.0) dt = 1e-6; /* safety fallback */
/* Standard normal random number (Box-Muller) */
real u1 = ((real)rand() + 1.0) / ((real)RAND_MAX + 2.0);
real u2 = ((real)rand() + 1.0) / ((real)RAND_MAX + 2.0);
real xi = sqrt(-2.0 * log(u1)) * cos(2.0 * PI * u2);
/* Brownian force magnitude per unit mass */
real sigma = sqrt(2.0 * KB * T * 3.0 * PI * mu * d) / (dt * P_MASS(p));
/* Return force component in direction i */
real force = sigma * xi;
return force; /* [m/s^2] */
}
Hooking instructions
1. Compile the UDF 2. Models > Discrete Phase > Physical Models > Enable Body Forces 3. In the Body Force dropdown for User-Defined, select brownian_force_pm25 4. Ensure Stochastic Tracking is enabled with sufficient Number of Tries (20+)
Verification
After running, check:
- DPM Summary: particles should not all follow identical paths (stochastic dispersion visible)
- Concentration contours: should show diffuse spread, not sharp filaments
- Histogram at outlet: particle arrival times should show a distribution
UDF Debugging Strategies
| Problem | Diagnostic | Fix |
| --------- | ------------ | ----- |
| UDF fires but particle does nothing | Return value not accumulated | Check that return value has correct sign and units |
| Particles go to infinity | Force too large or dt too large | Scale force by 1/mass; reduce Max. Number of Steps |
Access violation on C_T(c,t) | Particle not yet in a valid cell | Guard with NULLP(t) or check cell validity |
Error: received fatal signal | Array out of bounds | Check i index in DEFINE_DPM_BODY_FORCE is 0, 1, or 2 |
| UDF compiles but doesn't appear in dropdown | DEFINE_* macro name doesn't match hook name exactly | Spelling must be identical |
Wrong values in P_VEL(p) | Reading velocity at wrong tracking step | Use P_VEL0(p) for cell-entry values, P_VEL(p) for current |