Skip to content

Conversation

@fvitt
Copy link
Collaborator

@fvitt fvitt commented Jan 13, 2026

This provides the option to use the TUV-x library to compute photo-chemical rates in-line as an alternative to the traditional table look up method.

Closes #757

fvitt added 2 commits January 7, 2026 10:28
	modified:   .gitmodules
	modified:   cime_config/buildlib
	modified:   libraries/tuv-x (new commits)
Copy link
Member

@jimmielin jimmielin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @fvitt - I had some minor suggested changes that are hopefully straightforward.

call mee_ion_final()
call rate_diags_final()
call species_sums_final()
call tuvx_finalize()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could maybe wrap this within if (tuvx_active) to be consistent with the other calls

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

!-----------------------------------------------------------------
! ... get calculated photolysis rates from TUV-x
!-----------------------------------------------------------------
call tuvx_get_photo_rates( state, pbuf, ncol, lchnk, zmid, zint, &
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the only place tuvx_get_photo_rates uses state is to get state%ncol in line ~679, but ncol is already passed in here. Maybe state does not have to be passed in to call tuvx_get_photo_rates

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines 261 to 271
subroutine gas_phase_chemdr(lchnk, ncol, imozart, q, &
phis, zm, zi, calday, &
tfld, pmid, pdel, pint, rpdel, rpdeldry, &
cldw, troplev, troplevchem, &
ncldwtr, ufld, vfld, &
delt, ps, &
fsds, ts, asdir, ocnfrac, icefrac, &
precc, precl, snowhland, ghg_chem, latmapback, &
drydepflx, wetdepflx, cflx, fire_sflx, fire_ztop, nhx_nitrogen_flx, noy_nitrogen_flx, &
use_hemco, qtend, pbuf)
drydepflx, wetdepflx, cflx, fire_sflx, fire_ztop, &
nhx_nitrogen_flx, noy_nitrogen_flx, &
use_hemco, qtend, pbuf, state)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes appear to be unnecessary as state was already previously passed in as the first argument, and was moved to the last one for some reason.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

use mo_chm_diags, only : chm_diags, het_diags
use perf_mod, only : t_startf, t_stopf
use gas_wetdep_opts, only : gas_wetdep_method
use physics_types, only : physics_state
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is a duplicate of line 318 and does not need to be added again

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplicate line has been removed

Comment on lines 1276 to 1283
call gas_phase_chemdr(lchnk, ncol, imozart, state%q, &
state%phis, state%zm, state%zi, calday, &
state%t, state%pmid, state%pdel, state%pint, state%rpdel, state%rpdeldry, &
cldw, tropLev, tropLevChem, ncldwtr, state%u, state%v, chem_dt, state%ps, &
fsds, cam_in%ts, cam_in%asdir, cam_in%ocnfrac, cam_in%icefrac, &
cam_out%precc, cam_out%precl, cam_in%snowhland, ghg_chem, state%latmapback, &
drydepflx, wetdepflx, cam_in%cflx, cam_in%fireflx, cam_in%fireztop, &
nhx_nitrogen_flx, noy_nitrogen_flx, use_hemco, ptend%q, pbuf )
nhx_nitrogen_flx, noy_nitrogen_flx, use_hemco, ptend%q, pbuf, state )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes appear to be unnecessary as state was already previously passed in as the first argument, and was moved to the last one for some reason.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 52 to 53
integer, parameter :: PROFILE_INDEX_SO2 = 7 ! Sulfur dioxide profile index
integer, parameter :: PROFILE_INDEX_NO2 = 8 ! Nitrogen dioxide profile index
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These appear to be non-public and unused, can I ask if these are intended to be not implemented?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment on lines 60 to 63
! Definition of the MS93 wavelength grid TODO add description of this
integer, parameter :: NUM_BINS_MS93 = 4
real(kind=r8), parameter :: WAVELENGTH_EDGES_MS93(NUM_BINS_MS93+1) = &
(/ 181.6_r8, 183.1_r8, 184.6_r8, 190.2_r8, 192.5_r8 /)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed TODO here about the description here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a brief description

Comment on lines 451 to 454
deallocate( cam_grids )
deallocate( cam_profiles )
deallocate( cam_radiators )
deallocate( wavelength )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since wavelength => cam_grids%get_grid(...) I would suggest switching the deallocate order for safety

Suggested change
deallocate( cam_grids )
deallocate( cam_profiles )
deallocate( cam_radiators )
deallocate( wavelength )
deallocate( wavelength )
deallocate( cam_grids )
deallocate( cam_profiles )
deallocate( cam_radiators )

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


host_profile => profiles%get_profile( "temperature", "K" )
this%profiles_( PROFILE_INDEX_TEMPERATURE ) = this%core_%get_updater( host_profile, found )
call assert( 418735162, found )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and throughout the file, there are many calls to musica_assert::assert with seemingly random numbers for unique error codes. It seems to be consistent with what's done elsewhere in MUSICA, but I would request to at least have a comment somewhere that these numbers are just unique error codes and don't have any particular meaning other than being unique, and this is how the MUSICA library handles errors. Thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added comments

! || ||
! ||
! ----------------- i_int = pver
! ================= i_imd = pver ------------------ i_int = 2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo here:

Suggested change
! ================= i_imd = pver ------------------ i_int = 2
! ================= i_mid = pver ------------------ i_int = 2

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

	deleted:    src/chemistry/mozart/mo_setz.F90
	deleted:    src/chemistry/mozart/mo_tuv_inti.F90
	modified:   src/chemistry/mozart/chemistry.F90
	modified:   src/chemistry/mozart/mo_gas_phase_chemdr.F90
	modified:   src/chemistry/mozart/mo_tuvx.F90
@fvitt fvitt requested a review from jimmielin January 29, 2026 15:48
Copy link
Collaborator

@nusbaume nusbaume left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for bringing this in @fvitt! I had some cleanup requests in the python code, but otherwise only had a couple other comments. Hopefully none of my requests will cause any issues, but if you find that they do please let me know!

carma_maxretries = 40
ubc_specifier = 'T->MSIS', 'Q->2.d-8vmr', 'CH4->2.d-10vmr', 'H->MSIS', 'N->MSIS', 'O->MSIS', 'O2->MSIS', 'H2->TGCM', 'NO->SNOE'
tuvx_active = .true.
tuvx_config_path = '$SRCROOT/src/chemistry/pp_waccm_ma_sulfur/tuvx_config.json'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use $COMP_ROOT_DIR_ATM here instead of $SRCROOT? I am only asking because the other tests using TUV-x appear to use $COMP_ROOT_DIR_ATM.

'QCO2ext','TCO2ext','PCO2ext','CO2_ext','N2_ext','O_ext','O2_ext'

tuvx_active = .true.
tuvx_config_path = '$COMP_ROOT_DIR_ATM/src/chemistry/pp_waccm_ma_mam5/tuvx_config.json'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just-double checking that the config file under pp_waccm_ma_mam5 can also be used for a MAM4 test?

###############################################################################

rc, out, err = run_cmd(command, from_dir=working_dir, verbose=True)
expect(rc == 0, "Command {} failed with rc={}".format(command, rc))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add the out and err info to the expect call here as well (and convert it to an f-string):

Suggested change
expect(rc == 0, "Command {} failed with rc={}".format(command, rc))
expect(rc == 0, f"Command {command} failed with rc={rc} out={out} err={err}")

Comment on lines +177 to +188
args = "-DCONVERT_TO_MAKE=ON "
args += "-DCASEROOT={} ".format(caseroot)
args += "-DCOMPILER={} ".format(build.get_value("COMPILER"))
args += "-DOS={} ".format(build.get_value("OS"))
args += "-DMACH={} ".format(case.get_value("MACH"))
args += "-DCMAKE_C_COMPILER_WORKS=1 "
args += "-DCMAKE_Fortran_COMPILER_WORKS=1 "
args += "-DCMAKE_CXX_COMPILER_WORKS=1 "
args += "-DDEBUG={} ".format(build.get_value("DEBUG"))
cmd = "cmake {} .".format(args)
rc, out, err = run_cmd(cmd, combine_output=True, from_dir=macro_path)
expect(rc == 0, "Command {} failed with rc={} out={} err={}".format(cmd, rc, out, err))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would convert these all to f-strings:

Suggested change
args = "-DCONVERT_TO_MAKE=ON "
args += "-DCASEROOT={} ".format(caseroot)
args += "-DCOMPILER={} ".format(build.get_value("COMPILER"))
args += "-DOS={} ".format(build.get_value("OS"))
args += "-DMACH={} ".format(case.get_value("MACH"))
args += "-DCMAKE_C_COMPILER_WORKS=1 "
args += "-DCMAKE_Fortran_COMPILER_WORKS=1 "
args += "-DCMAKE_CXX_COMPILER_WORKS=1 "
args += "-DDEBUG={} ".format(build.get_value("DEBUG"))
cmd = "cmake {} .".format(args)
rc, out, err = run_cmd(cmd, combine_output=True, from_dir=macro_path)
expect(rc == 0, "Command {} failed with rc={} out={} err={}".format(cmd, rc, out, err))
args = "-DCONVERT_TO_MAKE=ON "
args += f"-DCASEROOT={caseroot} "
args += f"-DCOMPILER={build.get_value("COMPILER")} "
args += f"-DOS={build.get_value("OS")} "
args +=f "-DMACH={case.get_value("MACH")} "
args += "-DCMAKE_C_COMPILER_WORKS=1 "
args += "-DCMAKE_Fortran_COMPILER_WORKS=1 "
args += "-DCMAKE_CXX_COMPILER_WORKS=1 "
args += f"-DDEBUG={build.get_value("DEBUG")} "
cmd = f"cmake {args} ."
rc, out, err = run_cmd(cmd, combine_output=True, from_dir=macro_path)
expect(rc == 0, f"Command {cmd} failed with rc={rc} out={out} err={err}")

Comment on lines +213 to +223
arg_dict = _cmake_default_args(caseroot)
if build.get_value("MPILIB") == "mpi-serial":
cmake_args = "-DCMAKE_Fortran_COMPILER={} ".format(arg_dict["SFC"])
else:
cmake_args = "-DCMAKE_Fortran_COMPILER={} ".format(arg_dict["MPIFC"])
cmake_args += "-DTUVX_ENABLE_MPI:BOOL=TRUE "
if case.get_value("DEBUG"):
cmake_args += "-DCMAKE_BUILD_TYPE=Debug "
else:
cmake_args += "-DCMAKE_BUILD_TYPE=Release "
cmake_args += "-DCMAKE_VERBOSE_MAKEFILE=ON "
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like much of this Fortran compiler logic is immediately clobbered below. Given that I don't think CAM ever really uses the "mpi-serial" library, I might clean this up like so:

Suggested change
arg_dict = _cmake_default_args(caseroot)
if build.get_value("MPILIB") == "mpi-serial":
cmake_args = "-DCMAKE_Fortran_COMPILER={} ".format(arg_dict["SFC"])
else:
cmake_args = "-DCMAKE_Fortran_COMPILER={} ".format(arg_dict["MPIFC"])
cmake_args += "-DTUVX_ENABLE_MPI:BOOL=TRUE "
if case.get_value("DEBUG"):
cmake_args += "-DCMAKE_BUILD_TYPE=Debug "
else:
cmake_args += "-DCMAKE_BUILD_TYPE=Release "
cmake_args += "-DCMAKE_VERBOSE_MAKEFILE=ON "
arg_dict = _cmake_default_args(caseroot)
cmake_args = "-DCMAKE_VERBOSE_MAKEFILE=ON "
if build.get_value("MPILIB") != "mpi-serial":
cmake_args += "-DTUVX_ENABLE_MPI:BOOL=TRUE "
if case.get_value("DEBUG"):
cmake_args += "-DCMAKE_BUILD_TYPE=Debug "
else:
cmake_args += "-DCMAKE_BUILD_TYPE=Release "

Comment on lines +309 to +314
expect(len(corepaths)>0, \
"TUV-x not found at {}".format(libroot))
expect(len(corepaths)<2, \
"Multiple TUV-x versions found at {}".format(libroot))
expect(os.path.exists(corepaths[0]), \
"TUV-x install directory not found at {}".format(corepaths[0]))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would convert these error messages to f-strings:

Suggested change
expect(len(corepaths)>0, \
"TUV-x not found at {}".format(libroot))
expect(len(corepaths)<2, \
"Multiple TUV-x versions found at {}".format(libroot))
expect(os.path.exists(corepaths[0]), \
"TUV-x install directory not found at {}".format(corepaths[0]))
expect(len(corepaths)>0, \
f"TUV-x not found at {libroot}"
expect(len(corepaths)<2, \
f"Multiple TUV-x versions found at {libroot}"
expect(os.path.exists(corepaths[0]), \
f"TUV-x install directory not found at {corepaths[0]}"

return corepaths[0]

###############################################################################
def _tuvx_package_dir(libroot):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this function is actually used anywhere (?).

# copy geos-chem config files to rundir if using geos-chem chemistry
# -----------------------------------------------------

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for the extra whitespace here?

end if
call aurora_register()
endif
call tuvx_register( )
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to call tuvx_register() if tuvx_active is .false.? If not then I might update this to be:

Suggested change
call tuvx_register( )
if( tuvx_active ) call tuvx_register( )

character(len=2) :: mstring
character(len=7) :: jstring
logical :: do_jeuv
logical, save :: is_initialized = .false.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit-pick, but can we possibly move this up to be a protected module-level variable? I generally wouldn't expect saved variables to exist within a subroutine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants