NUOPC Integration¶
This guide covers coupling CATChem with the National Unified Operational Prediction Capability (NUOPC) software layer used in Earth system models. In this integration, CATChem is coupled to the atmosphere (ATM) component within the UFS and CATChem acts as any other modeling component within an Earth system model.
Overview¶
NUOPC integration enables CATChem to function as a component in coupled Earth system models, providing:
- ESMF/NUOPC Compliance - Standard NUOPC component interfaces
- Coupling Infrastructure - Standard field exchanges and time management
- Multi-Model Integration - Coupling with atmosphere, ocean, land, and ice models
- Scalable Parallelization - ESMF-based parallel decomposition
NUOPC Component Architecture¶
CATChem NUOPC Cap¶
module CATCHEM_NUOPC_Cap
use ESMF
use NUOPC
use NUOPC_Model, &
model_routine_SS => SetServices, &
model_label_Advance => label_Advance
use catchem_mod
implicit none
private
public :: SetServices
contains
subroutine SetServices(model, rc)
type(ESMF_GridComp) :: model
integer, intent(out) :: rc
! Register the generic methods
call NUOPC_CompDerive(model, model_routine_SS, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Set entry point for initialization phases
call NUOPC_CompSetEntryPoint(model, ESMF_METHOD_INITIALIZE, &
phaseLabelList=(/"IPDv00p1"/), userRoutine=InitializeP1, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call NUOPC_CompSetEntryPoint(model, ESMF_METHOD_INITIALIZE, &
phaseLabelList=(/"IPDv00p2"/), userRoutine=InitializeP2, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Set entry point for run phase
call NUOPC_CompSetEntryPoint(model, ESMF_METHOD_RUN, &
phaseLabelList=(/model_label_Advance/), userRoutine=ModelAdvance, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Set entry point for finalization
call NUOPC_CompSetEntryPoint(model, ESMF_METHOD_FINALIZE, &
phaseLabelList=(/"IPDv00p1"/), userRoutine=ModelFinalize, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
end subroutine SetServices
end module CATCHEM_NUOPC_Cap
Initialization Phases¶
subroutine InitializeP1(model, importState, exportState, clock, rc)
type(ESMF_GridComp) :: model
type(ESMF_State) :: importState, exportState
type(ESMF_Clock) :: clock
integer, intent(out) :: rc
! Advertise import and export fields
call NUOPC_Advertise(importState, StandardName="air_temperature", rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call NUOPC_Advertise(importState, StandardName="air_pressure", rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call NUOPC_Advertise(importState, StandardName="specific_humidity", rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call NUOPC_Advertise(exportState, StandardName="mass_fraction_of_ozone_in_air", rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call NUOPC_Advertise(exportState, StandardName="mass_fraction_of_nitrogen_dioxide_in_air", rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
end subroutine InitializeP1
subroutine InitializeP2(model, importState, exportState, clock, rc)
type(ESMF_GridComp) :: model
type(ESMF_State) :: importState, exportState
type(ESMF_Clock) :: clock
integer, intent(out) :: rc
type(ESMF_Grid) :: grid
type(ESMF_Field) :: field
type(ESMF_Config) :: config
character(len=ESMF_MAXSTR) :: config_file
integer :: localPet, petCount
! Get ESMF context
call ESMF_GridCompGet(model, config=config, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Read configuration
call ESMF_ConfigGetAttribute(config, config_file, &
label='catchem_config_file:', default='catchem_config.yml', rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Create grid (or get from parent)
call CreateCATChemGrid(model, grid, rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Realize import fields
call RealizeConnectedFields(importState, grid, "import", rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Realize export fields
call RealizeConnectedFields(exportState, grid, "export", rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Initialize CATChem
call InitializeCATChem(config_file, grid, rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
end subroutine InitializeP2
Model Advance¶
subroutine ModelAdvance(model, importState, exportState, clock, rc)
type(ESMF_GridComp) :: model
type(ESMF_State) :: importState, exportState
type(ESMF_Clock) :: clock
integer, intent(out) :: rc
type(ESMF_Time) :: currTime, stopTime
type(ESMF_TimeInterval) :: timeStep
type(ESMF_Field) :: field_temp, field_pres, field_humid
type(ESMF_Field) :: field_o3, field_no2
real(ESMF_KIND_R8), pointer :: temp_ptr(:,:,:), pres_ptr(:,:,:)
real(ESMF_KIND_R8), pointer :: humid_ptr(:,:,:)
real(ESMF_KIND_R8), pointer :: o3_ptr(:,:,:), no2_ptr(:,:,:)
real(ESMF_KIND_R8) :: dt_seconds
! Get current time and timestep
call ESMF_ClockGet(clock, currTime=currTime, timeStep=timeStep, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call ESMF_TimeIntervalGet(timeStep, s_r8=dt_seconds, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Get import fields
call ESMF_StateGet(importState, "air_temperature", field_temp, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call ESMF_StateGet(importState, "air_pressure", field_pres, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call ESMF_StateGet(importState, "specific_humidity", field_humid, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Get export fields
call ESMF_StateGet(exportState, "mass_fraction_of_ozone_in_air", field_o3, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call ESMF_StateGet(exportState, "mass_fraction_of_nitrogen_dioxide_in_air", field_no2, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Get field data pointers
call ESMF_FieldGet(field_temp, farrayPtr=temp_ptr, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call ESMF_FieldGet(field_pres, farrayPtr=pres_ptr, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call ESMF_FieldGet(field_humid, farrayPtr=humid_ptr, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call ESMF_FieldGet(field_o3, farrayPtr=o3_ptr, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
call ESMF_FieldGet(field_no2, farrayPtr=no2_ptr, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Run CATChem
call RunCATChemNUOPC(temp_ptr, pres_ptr, humid_ptr, &
o3_ptr, no2_ptr, dt_seconds, rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
end subroutine ModelAdvance
Configuration¶
NUOPC Configuration (nuopc_config.yml)¶
# NUOPC-specific CATChem configuration
nuopc_integration:
enabled: true
component_name: "CATCHEM"
# Grid configuration
grid:
type: "structured"
coordinate_system: "spherical"
decomposition: "2d_block"
# Field definitions with NUOPC standard names
import_fields:
- standard_name: "air_temperature"
long_name: "Air Temperature"
units: "K"
grid_location: "center"
- standard_name: "air_pressure"
long_name: "Air Pressure"
units: "Pa"
grid_location: "center"
- standard_name: "specific_humidity"
long_name: "Specific Humidity"
units: "kg kg-1"
grid_location: "center"
export_fields:
- standard_name: "mass_fraction_of_ozone_in_air"
long_name: "Ozone Mass Fraction"
units: "kg kg-1"
grid_location: "center"
- standard_name: "mass_fraction_of_nitrogen_dioxide_in_air"
long_name: "NO2 Mass Fraction"
units: "kg kg-1"
grid_location: "center"
# Coupling configuration
coupling:
coupling_interval: 3600 # seconds
interpolation_method: "bilinear"
extrapolation_method: "none"
# Performance settings
performance:
threading: "esmf_openmp"
decomposition_optimization: true
memory_allocation: "esmf_managed"
ESMF/NUOPC Application Configuration¶
program NUOPC_CATChem_App
use ESMF
use NUOPC
use NUOPC_Driver, only: driver_routine_SS => SetServices
implicit none
type(ESMF_GridComp) :: driver
integer :: rc, finalrc = ESMF_SUCCESS
! Initialize ESMF
call ESMF_Initialize(defaultlogfilename="NUOPC_CATChem_App.Log", &
logkindflag=ESMF_LOGKIND_MULTI, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) &
call ESMF_Finalize(endflag=ESMF_END_ABORT)
! Create driver component
driver = ESMF_GridCompCreate(name="NUOPC_CATChem_Driver", rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) &
call ESMF_Finalize(endflag=ESMF_END_ABORT)
! Set driver services
call ESMF_GridCompSetServices(driver, driver_routine_SS, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) &
call ESMF_Finalize(endflag=ESMF_END_ABORT)
! Initialize driver
call ESMF_GridCompInitialize(driver, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) &
call ESMF_Finalize(endflag=ESMF_END_ABORT)
! Run driver
call ESMF_GridCompRun(driver, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) &
call ESMF_Finalize(endflag=ESMF_END_ABORT)
! Finalize driver
call ESMF_GridCompFinalize(driver, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) &
finalrc = ESMF_FAILURE
! Destroy driver
call ESMF_GridCompDestroy(driver, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) &
finalrc = ESMF_FAILURE
! Finalize ESMF
call ESMF_Finalize(endflag=ESMF_END_NORMAL, rc=rc)
if (rc /= ESMF_SUCCESS) finalrc = ESMF_FAILURE
if (finalrc == ESMF_SUCCESS) then
print *, "NUOPC CATChem Application completed successfully"
else
print *, "NUOPC CATChem Application failed"
end if
end program NUOPC_CATChem_App
Advanced Features¶
Regridding and Interpolation¶
subroutine SetupRegridding(srcGrid, dstGrid, regridMethod, rc)
type(ESMF_Grid), intent(in) :: srcGrid, dstGrid
type(ESMF_RegridMethod_Flag), intent(in) :: regridMethod
integer, intent(out) :: rc
type(ESMF_RouteHandle) :: routeHandle
type(ESMF_Field) :: srcField, dstField
! Create fields for regridding
srcField = ESMF_FieldCreate(srcGrid, typekind=ESMF_TYPEKIND_R8, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
dstField = ESMF_FieldCreate(dstGrid, typekind=ESMF_TYPEKIND_R8, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Store regridding weights
call ESMF_FieldRegridStore(srcField, dstField, &
regridmethod=regridMethod, &
routeHandle=routeHandle, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Store route handle for later use
call StoreRouteHandle('chemistry_regrid', routeHandle, rc)
end subroutine SetupRegridding
Multi-Component Coupling¶
! Driver for coupled atmosphere-chemistry system
subroutine CoupledModelDriver(model, rc)
type(ESMF_GridComp), intent(inout) :: model
integer, intent(out) :: rc
type(ESMF_GridComp) :: atmComp, chemComp
type(ESMF_CplComp) :: connector
type(ESMF_State) :: atmExportState, chemImportState
type(ESMF_Clock) :: clock
! Create atmosphere component
atmComp = ESMF_GridCompCreate(name="ATM", rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Create chemistry component (CATChem)
chemComp = ESMF_GridCompCreate(name="CATCHEM", rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Create connector for coupling
connector = ESMF_CplCompCreate(name="ATM-CATCHEM", rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Setup coupling fields and dependencies
call SetupCouplingFields(atmComp, chemComp, connector, rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Run coupled system
call RunCoupledSystem(atmComp, chemComp, connector, clock, rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
end subroutine CoupledModelDriver
Testing and Validation¶
NUOPC Compliance Testing¶
program test_nuopc_compliance
use ESMF
use NUOPC
use CATCHEM_NUOPC_Cap
implicit none
type(ESMF_GridComp) :: comp
type(ESMF_State) :: importState, exportState
type(ESMF_Clock) :: clock
integer :: rc
logical :: compliance_passed = .true.
! Initialize ESMF
call ESMF_Initialize(rc=rc)
! Create test component
comp = ESMF_GridCompCreate(name="CATCHEM_TEST", rc=rc)
if (rc /= ESMF_SUCCESS) compliance_passed = .false.
! Test SetServices
call SetServices(comp, rc)
if (rc /= ESMF_SUCCESS) compliance_passed = .false.
! Create states
importState = ESMF_StateCreate(name="import", stateintent=ESMF_STATEINTENT_IMPORT, rc=rc)
exportState = ESMF_StateCreate(name="export", stateintent=ESMF_STATEINTENT_EXPORT, rc=rc)
! Create clock
call CreateTestClock(clock, rc)
if (rc /= ESMF_SUCCESS) compliance_passed = .false.
! Test initialization phases
call ESMF_GridCompInitialize(comp, importState=importState, &
exportState=exportState, clock=clock, phase=1, rc=rc)
if (rc /= ESMF_SUCCESS) compliance_passed = .false.
! Cleanup
call ESMF_GridCompDestroy(comp, rc=rc)
call ESMF_StateDestroy(importState, rc=rc)
call ESMF_StateDestroy(exportState, rc=rc)
call ESMF_ClockDestroy(clock, rc=rc)
call ESMF_Finalize(rc=rc)
if (compliance_passed) then
print *, "NUOPC compliance test PASSED"
else
print *, "NUOPC compliance test FAILED"
stop 1
end if
end program test_nuopc_compliance
Best Practices¶
1. ESMF Resource Management¶
! Always check return codes
call ESMF_FieldGet(field, farrayPtr=data_ptr, rc=rc)
if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU)) return
! Properly destroy ESMF objects
call ESMF_FieldDestroy(field, rc=rc)
call ESMF_GridDestroy(grid, rc=rc)
2. Standard Names¶
- Use CF-compliant standard names for all fields
- Follow NUOPC naming conventions
- Document all field mappings clearly
3. Performance¶
- Minimize field copies between components
- Use ESMF regridding for efficient interpolation
- Implement proper parallel decomposition
This NUOPC integration enables CATChem to work in coupled Earth system modeling frameworks with standard interfaces and efficient coupling capabilities.