Extending NPSS with Custom Components: A Practical Guide to User-Defined Components
NPSS User-Defined Components let you integrate proprietary Fortran or C subroutines directly into propulsion simulations without modifying the solver core. This guide covers the callback interface, compilation workflow, a turbine map example, and debugging strategies.
12 Jun 2026, 23:06 UTC

The Problem: Proprietary Physics in a Standard Solver
NPSS (Numerical Propulsion System Simulation) ships with a solid library of standard components—compressors, turbines, burners, nozzles. But every propulsion team eventually hits a wall: a proprietary turbine map that can't be shared, an experimental combustor correlation still under NDA, or a variable-geometry mechanism that doesn't map to any built-in element. Forking the NPSS core is a maintenance nightmare. The User-Defined Component (UDC) feature exists precisely for this gap: it lets you drop a compiled Fortran or C subroutine into a simulation without touching the solver code.
How the UDC Callback Interface Works
NPSS exposes three mandatory entry points that your subroutine must implement. The solver calls them at specific points in the solution sequence:
UDC_INIT— Runs once at startup. Use it to read configuration files, allocate workspace, or initialize lookup tables.UDC_CALC— Called every iteration. Receives inlet/outlet state arrays (pressure, temperature, flow rates, species fractions) and must return updated outlet states plus residuals for the solver's Newton loop.UDC_TERM— Runs at shutdown. Clean up file handles, deallocate memory, write final diagnostics.
The argument list is fixed by NPSS calling conventions. In Fortran, the signature looks like:
SUBROUTINE UDC_CALC(NIN, NOUT, IN_STATE, OUT_STATE, RESID, IERR)
INTEGER, INTENT(IN) :: NIN, NOUT
REAL*8, INTENT(IN) :: IN_STATE(NIN, *)
REAL*8, INTENT(OUT) :: OUT_STATE(NOUT, *)
REAL*8, INTENT(OUT) :: RESID(*)
INTEGER, INTENT(OUT) :: IERR
END SUBROUTINE
C implementations follow the same layout with double* arrays and int* for dimensions and error codes. Mismatched array dimensions or integer kinds are the most common source of silent mass/energy imbalances—NPSS won't validate them for you.
From Source File to Running Simulation
The workflow is deliberately low-ceremony but has sharp edges:
- Write the subroutine in a single
.f90or.cfile. Keep it self-contained; link against external libraries (e.g., a proprietary map DLL) at compile time. - Compile to a shared library. On Linux with GFortran:
Run this in your project's build directory. You need write permission there and the same compiler toolchain NPSS was built with—mixing GCC and Intel runtimes causes symbol conflicts.gfortran -shared -fPIC -O2 my_turbine_udc.f90 -o libmy_turbine.so - Reference it in the NPSS input deck using the
USERcomponent type:
TheCOMPONENT my_turbine USER LIBRARY = "libmy_turbine.so" INIT_ROUTINE = "UDC_INIT" CALC_ROUTINE = "UDC_CALC" TERM_ROUTINE = "UDC_TERM" N_IN = 4 N_OUT = 4 END COMPONENTN_IN/N_OUTvalues must match what yourUDC_CALCexpects. NPSS passes inlet stations first, then outlet stations. - Run the deck as usual:
npss my_model.nds. The solver loads the library viadlopen(orLoadLibraryon Windows) and resolves symbols at startup.
Worked Example: Embedding a Proprietary Turbine Map
Suppose your team has a 3D lookup table for a high-pressure turbine: corrected flow and efficiency as functions of corrected speed, pressure ratio, and variable stator angle. The table lives in a binary format only your Fortran reader understands.
In UDC_INIT, open the file, read the grid dimensions, and store the arrays in a module-level SAVE variable. In UDC_CALC, extract inlet total pressure/temperature and outlet static pressure from IN_STATE, compute corrected speed and pressure ratio, interpolate the map, then write corrected flow and efficiency to OUT_STATE. Return residuals as the difference between mapped flow and the solver's current flow guess.
Validation: run the deck at three operating points (takeoff, cruise, idle) and compare shaft power and exit temperature against the standalone map interpolation tool your team already trusts. Agreement within 0.5% on power and 2K on temperature is a reasonable first target.
Trade-offs and Debugging Realities
UDCs keep your IP out of the NPSS source tree, but they shift debugging burden to you. Errors inside UDC_CALC—array bounds violations, NaN propagation, division by zero—manifest as solver non-convergence or segmentation faults with no NPSS stack trace. Practical mitigations:
- Enable NPSS diagnostic output (
DEBUG = .TRUE.in the deck) to see inlet/outlet state vectors at each iteration. - Compile your UDC with
-g -fcheck=bounds -finit-real=nan(GCC) and run undergdb --args npss my_model.nds. Set a breakpoint atUDC_CALC. - Add internal consistency checks: verify mass flow in ≈ mass flow out, total enthalpy balance, and that residuals drive toward zero.
Portability note: the shared library must be recompiled for each target platform (Linux x86_64, Windows x64, possibly ARM). NPSS does not ship a UDC SDK; you're responsible for ABI compatibility.
Verifying Your Integration
Before trusting results, run the supplied sample_user_component example that ships with NPSS (typically in $NPSS_HOME/examples/user_component). Successful execution confirms the loader and callback chain work on your install. Then, for your own UDC:
- Check the linker log or run
nm -D libmy_turbine.so | grep UDC_to verify all three symbols are exported. - Run a steady-state case with
PRINT_RESIDUALS = .TRUE.and confirm residuals drop 3–4 orders of magnitude. - Do a transient ramp (e.g., throttle snap) and watch for energy drift—UDCs that don't enforce conservation internally can accumulate error over thousands of steps.
If the sample works but your UDC crashes, the fault is almost certainly in your subroutine's argument handling or memory management, not the NPSS loader.
Closing Action
Start small: wrap a single algebraic relation (e.g., a custom nozzle discharge coefficient) in a UDC, run it against the built-in nozzle component, and verify bit-for-bit match on a fixed operating point. Once the plumbing is proven, scale to the proprietary map. Keep the UDC source in version control alongside the NPSS deck—treat it as first-class model code, not a throwaway script.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.