grokkingstuff Home Blog Projects Wiki Calculators About

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

Interpreted vs Compiled

ModeWhen to useLimitations
--------------------------------
InterpretedQuick tests, simple expressionsNo #include, no structs, no pointers, limited C constructs
CompiledAll production DPM UDFsRequires 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)

MacroTypeReturns
----------------------
P_DIAM(p)realCurrent diameter [m]
P_VEL(p)real[3]Current velocity vector [m/s]
P_T(p)realCurrent temperature [K]
P_MASS(p)realCurrent mass [kg]
P_RHO(p)realCurrent density [kg/m^3]
P_TIME(p)realTime since injection [s]
P_LF(p)realLiquid fraction [0--1]
P_VFF(p)realVolatile fraction [0--1]
P_CELL(p)cell_tCurrent cell index
P_THREAD(p)Thread *Current cell thread
P_FACE(p)face_tCurrent 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:

MacroReturns
----------------
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:

MacroReturns
----------------
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

MacroReturns
----------------
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 valueMeaning
-----------------------
PATH_ACTIVEContinue tracking
PATH_ABORTKill the particle (absorbed)
PATH_ESCAPEMark 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

ErrorCauseFix
-------------------
error C2065: 'P_DIAM': undeclared identifierMissing #include "udf.h"Add the include
error LNK2019: unresolved external symbolFunction name mismatch between DEFINE_* and the hook nameCheck 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 UDFsRestrict 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

ContextCell UDMFace UDM
-----------------------------
ReadC_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:

UDF Debugging Strategies

ProblemDiagnosticFix
--------------------------
UDF fires but particle does nothingReturn value not accumulatedCheck that return value has correct sign and units
Particles go to infinityForce too large or dt too largeScale force by 1/mass; reduce Max. Number of Steps
Access violation on C_T(c,t)Particle not yet in a valid cellGuard with NULLP(t) or check cell validity
Error: received fatal signalArray out of boundsCheck i index in DEFINE_DPM_BODY_FORCE is 0, 1, or 2
UDF compiles but doesn't appear in dropdownDEFINE_* macro name doesn't match hook name exactlySpelling must be identical
Wrong values in P_VEL(p)Reading velocity at wrong tracking stepUse P_VEL0(p) for cell-entry values, P_VEL(p) for current

See Also