diff --git a/.gitignore b/.gitignore
index 04af4cbc..7d6f4be2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@ test_driver_*.sh
*~
.#*
\#*#
+**/.vscode/
diff --git a/src/control/cam_comp.F90 b/src/control/cam_comp.F90
index 8e12e405..0a8b1b63 100644
--- a/src/control/cam_comp.F90
+++ b/src/control/cam_comp.F90
@@ -84,25 +84,27 @@ subroutine cam_init(caseid, ctitle, model_doi_url, &
!
!-----------------------------------------------------------------------
- use cam_initfiles, only: cam_initfiles_open
- use dyn_grid, only: model_grid_init
- use phys_comp, only: phys_init
- use phys_comp, only: phys_register
- use dyn_comp, only: dyn_init
-! use cam_restart, only: cam_read_restart
- use camsrfexch, only: hub2atm_alloc, atm2hub_alloc
- use cam_history, only: history_init_files
-! use history_scam, only: scm_intht
- use cam_pio_utils, only: init_pio_subsystem
- use cam_instance, only: inst_suffix
-! use history_defaults, only: initialize_iop_history
- use stepon, only: stepon_init
- use air_composition, only: air_composition_init
- use cam_ccpp_cap, only: cam_ccpp_initialize_constituents
- use physics_grid, only: columns_on_task
- use vert_coord, only: pver
- use phys_vars_init_check, only: mark_as_initialized
- use tropopause_climo_read, only: tropopause_climo_read_file
+ use cam_initfiles, only: cam_initfiles_open
+ use dyn_grid, only: model_grid_init
+ use phys_comp, only: phys_init
+ use phys_comp, only: phys_register
+ use dyn_comp, only: dyn_init
+! use cam_restart, only: cam_read_restart
+ use camsrfexch, only: hub2atm_alloc, atm2hub_alloc
+ use cam_history, only: history_init_files
+! use history_scam, only: scm_intht
+ use cam_pio_utils, only: init_pio_subsystem
+ use cam_instance, only: inst_suffix
+! use history_defaults, only: initialize_iop_history
+ use stepon, only: stepon_init
+ use air_composition, only: air_composition_init
+ use cam_ccpp_cap, only: cam_ccpp_initialize_constituents
+ use physics_grid, only: columns_on_task
+ use vert_coord, only: pver
+ use phys_vars_init_check, only: mark_as_initialized
+ use tropopause_climo_read, only: tropopause_climo_read_file
+ use musica_ccpp_dependencies, only: musica_ccpp_dependencies_init
+ use orbital_data, only: orbital_data_init
! Arguments
character(len=cl), intent(in) :: caseid ! case ID
@@ -259,6 +261,16 @@ subroutine cam_init(caseid, ctitle, model_doi_url, &
! end if
call history_init_files(model_doi_url, caseid, ctitle)
+ ! Temporary: Prescribe realistic but inaccurate physical quantities
+ ! necessary for MUSICA that are currently unavailable in CAM-SIMA.
+ !
+ ! Remove this when MUSICA input data are available from CAM-SIMA or
+ ! other physics schemes.
+ call musica_ccpp_dependencies_init(columns_on_task, pver, iulog)
+
+ ! Initialize orbital data
+ call orbital_data_init(columns_on_task)
+
end subroutine cam_init
!
@@ -271,8 +283,16 @@ subroutine cam_timestep_init()
!
!-----------------------------------------------------------------------
- use phys_comp, only: phys_timestep_init
- use stepon, only: stepon_timestep_init
+ use phys_comp, only: phys_timestep_init
+ use physics_grid, only: lat_rad, lon_rad
+ use orbital_data, only: orbital_data_advance
+ use stepon, only: stepon_timestep_init
+
+ ! Update current fractional calendar day. Needs to be updated at every timestep.
+ calday = get_curr_calday()
+
+ ! Update the orbital data
+ call orbital_data_advance(calday, lat_rad, lon_rad)
! Update timestep flags in physics state
is_first_timestep = is_first_step()
@@ -294,9 +314,6 @@ subroutine cam_timestep_init()
!
call phys_timestep_init()
- ! Update current fractional calendar day. Needs to be updated at every timestep.
- calday = get_curr_calday()
-
end subroutine cam_timestep_init
!
!-----------------------------------------------------------------------
diff --git a/src/data/registry.xml b/src/data/registry.xml
index 91658e20..f1e02fbc 100644
--- a/src/data/registry.xml
+++ b/src/data/registry.xml
@@ -9,6 +9,8 @@
$SRCROOT/src/control/camsrfexch.meta
$SRCROOT/src/control/runtime_obj.meta
$SRCROOT/src/data/physconst.meta
+ $SRCROOT/src/physics/utils/orbital_data.meta
+ $SRCROOT/src/physics/utils/musica_ccpp_dependencies.meta
$SRCROOT/src/physics/utils/physics_grid.meta
$SRCROOT/src/physics/utils/cam_constituents.meta
$SRCROOT/src/physics/utils/tropopause_climo_read.meta
diff --git a/src/physics/ncar_ccpp b/src/physics/ncar_ccpp
index 491e5624..45c37530 160000
--- a/src/physics/ncar_ccpp
+++ b/src/physics/ncar_ccpp
@@ -1 +1 @@
-Subproject commit 491e56247815ef23bfd8dba65d1e3c3b78ba164a
+Subproject commit 45c37530ead4f5b93621e3d979e972ea91217923
diff --git a/src/physics/utils/musica_ccpp_dependencies.F90 b/src/physics/utils/musica_ccpp_dependencies.F90
new file mode 100644
index 00000000..3bd53167
--- /dev/null
+++ b/src/physics/utils/musica_ccpp_dependencies.F90
@@ -0,0 +1,197 @@
+! Copyright (C) 2024 National Science Foundation-National Center for Atmospheric Research
+! SPDX-License-Identifier: Apache-2.0
+module musica_ccpp_dependencies
+!--------------------------------------------------------------------------
+!
+! This module temporarily provides data that MUSICA chemistry consumes but
+! does not produce. The values are realistic but are not based on the
+! actual model state. These should be removed as the producers of this data
+! are added to CAM-SIMA or as CCPP-compliant physics schemes.
+!
+! IMPORTANT: This module must be completely removed before doing any actual
+! science with MUSICA chemistry in CAM-SIMA.
+!
+!--------------------------------------------------------------------------
+
+ use ccpp_kinds, only: kind_phys
+
+ implicit none
+ private
+
+ public :: musica_ccpp_dependencies_init
+
+ !> \section arg_table_musica_ccpp_dependencies Argument Table
+ !! \htmlinclude arg_table_musica_ccpp_dependencies.html
+ !!
+ integer, public, protected :: photolysis_wavelength_grid_section_dimension = 102
+ integer, public, protected :: photolysis_wavelength_grid_interface_dimension = 103
+ real(kind_phys), allocatable, public, protected :: photolysis_wavelength_grid_interfaces(:)
+ real(kind_phys), allocatable, public, protected :: extraterrestrial_radiation_flux(:)
+ real(kind_phys), allocatable, public, protected :: surface_albedo(:)
+ real(kind_phys), allocatable, public, protected :: blackbody_temperature_at_surface(:)
+ real(kind_phys), allocatable, public, protected :: cloud_area_fraction(:,:)
+
+ ! local parameters
+ character(len=*), parameter :: module_name = '(musica_ccpp_dependencies)'
+
+!==============================================================================
+contains
+!==============================================================================
+
+ subroutine musica_ccpp_dependencies_init(horizontal_dimension, &
+ vertical_layer_dimension, log_file_unit)
+
+ use cam_abortutils, only: check_allocate
+
+ !-----------------------------------------------------------------------
+ !
+ ! Initialize the MUSICA scheme dependencies.
+ !
+ !-----------------------------------------------------------------------
+
+ integer, intent(in) :: horizontal_dimension
+ integer, intent(in) :: vertical_layer_dimension
+ integer, intent(in) :: log_file_unit
+
+ integer :: error_code
+ character(len=*), parameter :: subroutine_name = &
+ trim(module_name)//':(musica_ccpp_dependencies_init)'
+
+ write(log_file_unit,*) 'WARNING: Using placeholder data for MUSICA chemistry.'
+
+ allocate(photolysis_wavelength_grid_interfaces(photolysis_wavelength_grid_interface_dimension), &
+ stat=error_code)
+ call check_allocate(error_code, subroutine_name, &
+ 'photolysis_wavelength_grid_interfaces(photolysis_wavelength_grid_interface_dimension)', &
+ file=__FILE__, line=__LINE__)
+ allocate(extraterrestrial_radiation_flux(photolysis_wavelength_grid_section_dimension), &
+ stat=error_code)
+ call check_allocate(error_code, subroutine_name, &
+ 'extraterrestrial_radiation_flux(photolysis_wavelength_grid_section_dimension)', &
+ file=__FILE__, line=__LINE__)
+ allocate(surface_albedo(horizontal_dimension), stat=error_code)
+ call check_allocate(error_code, subroutine_name, &
+ 'surface_albedo(horizontal_dimension)', &
+ file=__FILE__, line=__LINE__)
+ allocate(blackbody_temperature_at_surface(horizontal_dimension), stat=error_code)
+ call check_allocate(error_code, subroutine_name, &
+ 'blackbody_temperature_at_surface(horizontal_dimension)', &
+ file=__FILE__, line=__LINE__)
+ allocate(cloud_area_fraction(horizontal_dimension, vertical_layer_dimension), stat=error_code)
+ call check_allocate(error_code, subroutine_name, &
+ 'cloud_area_fraction(horizontal_dimension, vertical_layer_dimension)', &
+ file=__FILE__, line=__LINE__)
+
+ surface_albedo(:) = 0.1_kind_phys
+ blackbody_temperature_at_surface(:) = 292.3_kind_phys
+ cloud_area_fraction(:,:) = 0.7_kind_phys
+ extraterrestrial_radiation_flux(:) = 1.0e14_kind_phys
+ photolysis_wavelength_grid_interfaces = (/ &
+ 120.0e-9_kind_phys, &
+ 121.4e-9_kind_phys, &
+ 121.9e-9_kind_phys, &
+ 123.5e-9_kind_phys, &
+ 124.3e-9_kind_phys, &
+ 125.5e-9_kind_phys, &
+ 126.3e-9_kind_phys, &
+ 127.1e-9_kind_phys, &
+ 130.1e-9_kind_phys, &
+ 131.1e-9_kind_phys, &
+ 135.0e-9_kind_phys, &
+ 140.0e-9_kind_phys, &
+ 145.0e-9_kind_phys, &
+ 150.0e-9_kind_phys, &
+ 155.0e-9_kind_phys, &
+ 160.0e-9_kind_phys, &
+ 165.0e-9_kind_phys, &
+ 168.0e-9_kind_phys, &
+ 171.0e-9_kind_phys, &
+ 173.0e-9_kind_phys, &
+ 174.4e-9_kind_phys, &
+ 175.4e-9_kind_phys, &
+ 177.0e-9_kind_phys, &
+ 178.6e-9_kind_phys, &
+ 180.2e-9_kind_phys, &
+ 181.8e-9_kind_phys, &
+ 183.5e-9_kind_phys, &
+ 185.2e-9_kind_phys, &
+ 186.9e-9_kind_phys, &
+ 188.7e-9_kind_phys, &
+ 190.5e-9_kind_phys, &
+ 192.3e-9_kind_phys, &
+ 194.2e-9_kind_phys, &
+ 196.1e-9_kind_phys, &
+ 198.0e-9_kind_phys, &
+ 200.0e-9_kind_phys, &
+ 202.0e-9_kind_phys, &
+ 204.1e-9_kind_phys, &
+ 206.2e-9_kind_phys, &
+ 208.0e-9_kind_phys, &
+ 211.0e-9_kind_phys, &
+ 214.0e-9_kind_phys, &
+ 217.0e-9_kind_phys, &
+ 220.0e-9_kind_phys, &
+ 223.0e-9_kind_phys, &
+ 226.0e-9_kind_phys, &
+ 229.0e-9_kind_phys, &
+ 232.0e-9_kind_phys, &
+ 235.0e-9_kind_phys, &
+ 238.0e-9_kind_phys, &
+ 241.0e-9_kind_phys, &
+ 244.0e-9_kind_phys, &
+ 247.0e-9_kind_phys, &
+ 250.0e-9_kind_phys, &
+ 253.0e-9_kind_phys, &
+ 256.0e-9_kind_phys, &
+ 259.0e-9_kind_phys, &
+ 263.0e-9_kind_phys, &
+ 267.0e-9_kind_phys, &
+ 271.0e-9_kind_phys, &
+ 275.0e-9_kind_phys, &
+ 279.0e-9_kind_phys, &
+ 283.0e-9_kind_phys, &
+ 287.0e-9_kind_phys, &
+ 291.0e-9_kind_phys, &
+ 295.0e-9_kind_phys, &
+ 298.5e-9_kind_phys, &
+ 302.5e-9_kind_phys, &
+ 305.5e-9_kind_phys, &
+ 308.5e-9_kind_phys, &
+ 311.5e-9_kind_phys, &
+ 314.5e-9_kind_phys, &
+ 317.5e-9_kind_phys, &
+ 322.5e-9_kind_phys, &
+ 327.5e-9_kind_phys, &
+ 332.5e-9_kind_phys, &
+ 337.5e-9_kind_phys, &
+ 342.5e-9_kind_phys, &
+ 347.5e-9_kind_phys, &
+ 350.0e-9_kind_phys, &
+ 355.0e-9_kind_phys, &
+ 360.0e-9_kind_phys, &
+ 365.0e-9_kind_phys, &
+ 370.0e-9_kind_phys, &
+ 375.0e-9_kind_phys, &
+ 380.0e-9_kind_phys, &
+ 385.0e-9_kind_phys, &
+ 390.0e-9_kind_phys, &
+ 395.0e-9_kind_phys, &
+ 400.0e-9_kind_phys, &
+ 405.0e-9_kind_phys, &
+ 410.0e-9_kind_phys, &
+ 415.0e-9_kind_phys, &
+ 420.0e-9_kind_phys, &
+ 430.0e-9_kind_phys, &
+ 440.0e-9_kind_phys, &
+ 450.0e-9_kind_phys, &
+ 500.0e-9_kind_phys, &
+ 550.0e-9_kind_phys, &
+ 600.0e-9_kind_phys, &
+ 650.0e-9_kind_phys, &
+ 700.0e-9_kind_phys, &
+ 750.0e-9_kind_phys &
+ /)
+
+ end subroutine musica_ccpp_dependencies_init
+
+end module musica_ccpp_dependencies
\ No newline at end of file
diff --git a/src/physics/utils/musica_ccpp_dependencies.meta b/src/physics/utils/musica_ccpp_dependencies.meta
new file mode 100644
index 00000000..0d14908e
--- /dev/null
+++ b/src/physics/utils/musica_ccpp_dependencies.meta
@@ -0,0 +1,48 @@
+[ccpp-table-properties]
+ name = musica_ccpp_dependencies
+ type = module
+[ccpp-arg-table]
+ name = musica_ccpp_dependencies
+ type = module
+[ photolysis_wavelength_grid_section_dimension ]
+ standard_name = photolysis_wavelength_grid_section_dimension
+ units = count
+ type = integer
+ dimensions = ()
+ protected = True
+[ photolysis_wavelength_grid_interface_dimension ]
+ standard_name = photolysis_wavelength_grid_interface_dimension
+ units = count
+ type = integer
+ dimensions = ()
+ protected = True
+[ photolysis_wavelength_grid_interfaces ]
+ standard_name = photolysis_wavelength_grid_interfaces
+ units = m
+ type = real | kind = kind_phys
+ dimensions = (photolysis_wavelength_grid_interface_dimension)
+ protected = True
+[ extraterrestrial_radiation_flux ]
+ standard_name = extraterrestrial_radiation_flux
+ units = photons cm-2 s-1 nm-1
+ type = real | kind = kind_phys
+ dimensions = (photolysis_wavelength_grid_section_dimension)
+ protected = True
+[ surface_albedo ]
+ standard_name = surface_albedo_due_to_UV_and_VIS_direct
+ units = none
+ type = real | kind = kind_phys
+ dimensions = (horizontal_dimension)
+ protected = True
+[ blackbody_temperature_at_surface ]
+ standard_name = blackbody_temperature_at_surface
+ type = real | kind = kind_phys
+ units = K
+ dimensions = (horizontal_dimension)
+ protected = True
+[ cloud_area_fraction ]
+ standard_name = cloud_area_fraction
+ type = real | kind = kind_phys
+ units = fraction
+ dimensions = (horizontal_dimension,vertical_layer_dimension)
+ protected = True
diff --git a/src/physics/utils/orbital_data.F90 b/src/physics/utils/orbital_data.F90
new file mode 100644
index 00000000..df9f852d
--- /dev/null
+++ b/src/physics/utils/orbital_data.F90
@@ -0,0 +1,90 @@
+! Copyright (C) 2024 National Science Foundation-National Center for Atmospheric Research
+! SPDX-License-Identifier: Apache-2.0
+module orbital_data
+!--------------------------------------------------------------------------
+!
+! Provides access to conditions calculated based on the Earth's orbit.
+!
+!--------------------------------------------------------------------------
+
+ use ccpp_kinds, only: kind_phys
+ use shr_orb_mod, only: FILL_R8 => SHR_ORB_UNDEF_REAL
+
+ implicit none
+ private
+
+ public :: orbital_data_init, orbital_data_advance
+
+ !> \section arg_table_orbital_data Argument Table
+ !! \htmlinclude arg_table_orbital_data.html
+ !!
+ real(kind_phys), protected, public :: solar_declination = FILL_R8 ! Solar declination angle [radians]
+ real(kind_phys), protected, public :: earth_sun_distance = FILL_R8 ! Earth-sun distance [AU]
+ real(kind_phys), allocatable, protected, public :: solar_zenith_angle(:) ! Solar zenith angle (column) [radians]
+
+ ! Local parameters
+ character(len=*), parameter :: module_name = '(orbital_data)'
+
+!=======================================================================
+contains
+!=======================================================================
+
+ subroutine orbital_data_init(number_of_columns)
+
+ use cam_abortutils, only: check_allocate
+
+ !-----------------------------------------------------------------------
+ !
+ ! Initialize the orbital data module.
+ !
+ !-----------------------------------------------------------------------
+
+ integer, intent(in) :: number_of_columns
+
+ integer :: error_code
+ character(len=*), parameter :: subroutine_name = &
+ trim(module_name)//':(orbital_data_init)'
+
+ allocate(solar_zenith_angle(number_of_columns), source=FILL_R8, &
+ stat=error_code)
+ call check_allocate(error_code, subroutine_name, &
+ 'solar_zenith_angle(number_of_columns)', &
+ file=__FILE__, line=__LINE__)
+
+ end subroutine orbital_data_init
+
+ !=======================================================================
+
+ subroutine orbital_data_advance(calendar_day, latitudes, longitudes)
+
+ !-----------------------------------------------------------------------
+ !
+ ! Advance the orbital data to the current simulation time.
+ !
+ !-----------------------------------------------------------------------
+
+ use shr_orb_mod, only: shr_orb_decl, shr_orb_cosz
+ use cam_control_mod, only: eccen, mvelpp, lambm0, obliqr
+
+ real(kind_phys), intent(in) :: calendar_day ! Fractional Julian calendar day (1.xx to 365.xx)
+ real(kind_phys), intent(in) :: latitudes(:) ! Centered latitude (column) [radians]
+ real(kind_phys), intent(in) :: longitudes(:) ! Centered longitude (column) [radians]
+
+ integer :: i
+
+ ! Compute the solar declination angle [radians] and Earth-sun distance [AU]
+ call shr_orb_decl(calendar_day, eccen, mvelpp, lambm0, obliqr, &
+ solar_declination, earth_sun_distance)
+
+ ! Compute the solar zenith angle [radians]
+ do i = 1, size(latitudes)
+ solar_zenith_angle(i) = acos(shr_orb_cosz(calendar_day, latitudes(i), &
+ longitudes(i), solar_declination))
+ end do
+
+ end subroutine orbital_data_advance
+
+ !=======================================================================
+
+end module orbital_data
+
diff --git a/src/physics/utils/orbital_data.meta b/src/physics/utils/orbital_data.meta
new file mode 100644
index 00000000..611f7535
--- /dev/null
+++ b/src/physics/utils/orbital_data.meta
@@ -0,0 +1,21 @@
+[ccpp-table-properties]
+ name = orbital_data
+ type = module
+[ccpp-arg-table]
+ name = orbital_data
+ type = module
+[solar_declination]
+ standard_name = solar_declination
+ units = rad
+ type = real | kind = kind_phys
+ dimensions = ()
+[earth_sun_distance]
+ standard_name = earth_sun_distance
+ units = AU
+ type = real | kind = kind_phys
+ dimensions = ()
+[solar_zenith_angle]
+ standard_name = solar_zenith_angle
+ units = rad
+ type = real | kind = kind_phys
+ dimensions = (horizontal_dimension)
\ No newline at end of file
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
new file mode 100644
index 00000000..21b46f77
--- /dev/null
+++ b/test/CMakeLists.txt
@@ -0,0 +1,25 @@
+cmake_minimum_required(VERSION 3.21)
+
+project(
+ cam-sima-tests
+ VERSION 0.0.0
+ LANGUAGES Fortran
+)
+
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH};${CMAKE_CURRENT_LIST_DIR}/cmake)
+set(CMAKE_USER_MAKE_RULES_OVERRIDE ${CMAKE_MODULE_PATH}/SetDefaults.cmake)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
+
+option(CCPP_ENABLE_MEMCHECK "Enable memory checks in tests" OFF)
+
+set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
+set(BUILD_GMOCK OFF CACHE BOOL "" FORCE)
+
+set(CAM_SIMA_SRC_PATH ${CMAKE_SOURCE_DIR}/../src)
+set(CAM_SIMA_TEST_PATH ${CMAKE_SOURCE_DIR})
+
+include(TestUtils)
+include(CTest)
+enable_testing()
+
+add_subdirectory(unit)
\ No newline at end of file
diff --git a/test/cmake/SetDefaults.cmake b/test/cmake/SetDefaults.cmake
new file mode 100644
index 00000000..39e5d1d5
--- /dev/null
+++ b/test/cmake/SetDefaults.cmake
@@ -0,0 +1,4 @@
+# Overwrite the init values choosen by CMake
+if (CMAKE_Fortran_COMPILER_ID MATCHES "GNU")
+ set(CMAKE_Fortran_FLAGS_DEBUG_INIT "-g")
+endif()
diff --git a/test/cmake/TestUtils.cmake b/test/cmake/TestUtils.cmake
new file mode 100644
index 00000000..df31b9cd
--- /dev/null
+++ b/test/cmake/TestUtils.cmake
@@ -0,0 +1,23 @@
+################################################################################
+# Utility functions for creating tests
+
+if(CCPP_ENABLE_MEMCHECK)
+ find_program(MEMORYCHECK_COMMAND "valgrind")
+
+ # Set the Valgrind suppressions file for tests
+ set(MEMCHECK_SUPPRESS "--suppressions=${CMAKE_SOURCE_DIR}/valgrind.supp")
+endif()
+
+################################################################################
+# Runs a test with memory checking if enabled
+
+function(add_memory_check_test test_name test_binary test_args working_dir)
+ if(CCPP_ENABLE_MEMCHECK)
+ add_test(NAME memcheck_${test_name}
+ COMMAND mpirun -v -np 1 ${MEMORYCHECK_COMMAND} --leak-check=full --error-exitcode=1 --trace-children=yes --gen-suppressions=all ${MEMCHECK_SUPPRESS}
+ ${test_binary} ${test_args}
+ WORKING_DIRECTORY ${working_dir})
+ endif()
+endfunction(add_memory_check_test)
+
+################################################################################
\ No newline at end of file
diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile
new file mode 100644
index 00000000..474be7e5
--- /dev/null
+++ b/test/docker/Dockerfile
@@ -0,0 +1,40 @@
+# This Dockerfile is designed for running CAM-SIMA fortran unit tests and integration tests.
+FROM ubuntu:22.04
+
+ARG BUILD_TYPE=Debug
+
+RUN apt update \
+ && apt install -y sudo \
+ && useradd -m test_user \
+ && echo "test_user ALL=(root) NOPASSWD: ALL" >> /etc/sudoers.d/test_user \
+ && chmod 0440 /etc/sudoers.d/test_user
+
+USER test_user
+WORKDIR /home/test_user
+
+RUN sudo apt update \
+ && sudo apt -y install \
+ cmake \
+ cmake-curses-gui \
+ gfortran \
+ git \
+ libopenmpi-dev \
+ make \
+ openmpi-bin \
+ valgrind \
+ vim \
+ && sudo apt clean
+
+ENV FC=mpif90
+ENV FFLAGS="-I/usr/include/"
+
+COPY . cam_sima
+RUN sudo chown -R test_user:test_user cam_sima
+
+RUN cd cam_sima/test \
+ && cmake -S . -B build \
+ -D CMAKE_BUILD_TYPE=${BUILD_TYPE} \
+ -D CCPP_ENABLE_MEMCHECK=ON \
+ && cmake --build ./build
+
+WORKDIR /home/test_user/cam_sima/test/build
\ No newline at end of file
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
new file mode 100644
index 00000000..e220ee79
--- /dev/null
+++ b/test/unit/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(physics)
\ No newline at end of file
diff --git a/test/unit/physics/CMakeLists.txt b/test/unit/physics/CMakeLists.txt
new file mode 100644
index 00000000..47202fc6
--- /dev/null
+++ b/test/unit/physics/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(utils)
\ No newline at end of file
diff --git a/test/unit/physics/utils/CMakeLists.txt b/test/unit/physics/utils/CMakeLists.txt
new file mode 100644
index 00000000..abf72100
--- /dev/null
+++ b/test/unit/physics/utils/CMakeLists.txt
@@ -0,0 +1,17 @@
+# Test orbital_data module
+add_executable(test_physics_utils_orbital_data test_orbital_data.F90 test_orbital_data_stubs.F90)
+
+target_sources(test_physics_utils_orbital_data
+ PUBLIC
+ ${CAM_SIMA_SRC_PATH}/physics/utils/orbital_data.F90
+)
+
+target_compile_options(test_physics_utils_orbital_data PRIVATE -ffree-line-length-none)
+
+add_test(
+ NAME test_physics_utils_orbital_data
+ COMMAND $
+ WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
+)
+
+add_memory_check_test(test_physics_utils_orbital_data $ "" ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
\ No newline at end of file
diff --git a/test/unit/physics/utils/test_orbital_data.F90 b/test/unit/physics/utils/test_orbital_data.F90
new file mode 100644
index 00000000..d04c5573
--- /dev/null
+++ b/test/unit/physics/utils/test_orbital_data.F90
@@ -0,0 +1,57 @@
+! Copyright (C) 2024 National Science Foundation-National Center for Atmospheric Research
+! SPDX-License-Identifier: Apache-2.0
+!--------------------------------------------------------------------------
+!
+! Tests of the orbital_data module.
+!
+!--------------------------------------------------------------------------
+
+! Assert macros
+#define ASSERT(x) if (.not.(x)) then; write(*,*) "Assertion failed[", __FILE__, ":", __LINE__, "]: x"; stop 1; endif
+#define ASSERT_NEAR( a, b, abs_error ) if( (abs(a - b) >= abs_error) .and. (abs(a - b) /= 0.0) ) then; write(*,*) "Assertion failed[", __FILE__, ":", __LINE__, "]: a, b"; stop 1; endif
+
+program test_orbital_data
+
+ implicit none
+
+ call test_orbital_data_functions()
+
+contains
+
+ subroutine test_orbital_data_functions
+
+ use orbital_data
+ use shr_kind_mod, only: R8 => SHR_KIND_R8
+ implicit none
+
+ integer, parameter :: NUMBER_OF_COLUMNS = 3
+ real(kind=R8) :: calendar_day
+ real(kind=R8) :: latitudes(NUMBER_OF_COLUMNS)
+ real(kind=R8) :: longitudes(NUMBER_OF_COLUMNS)
+ integer :: i
+
+ calendar_day = 10.5_R8
+ latitudes = [1.0_R8, 2.0_R8, 3.0_R8]
+ longitudes = [4.0_R8, 5.0_R8, 6.0_R8]
+
+ call orbital_data_init(NUMBER_OF_COLUMNS)
+
+ ASSERT(allocated(solar_zenith_angle))
+ ASSERT(size(solar_zenith_angle) == NUMBER_OF_COLUMNS)
+ ASSERT(all(solar_zenith_angle == -1.0_R8))
+ ASSERT(solar_declination == -1.0_R8)
+ ASSERT(earth_sun_distance == -1.0_R8)
+
+ call orbital_data_advance(calendar_day, latitudes, longitudes)
+
+ ASSERT(allocated(solar_zenith_angle))
+ ASSERT(size(solar_zenith_angle) == NUMBER_OF_COLUMNS)
+ ASSERT_NEAR(solar_zenith_angle(1), acos(latitudes(1) + longitudes(1) + calendar_day * 21.0_R8), 1.0E-6_R8)
+ ASSERT_NEAR(solar_zenith_angle(2), acos(latitudes(2) + longitudes(2) + calendar_day * 21.0_R8), 1.0E-6_R8)
+ ASSERT_NEAR(solar_zenith_angle(3), acos(latitudes(3) + longitudes(3) + calendar_day * 21.0_R8), 1.0E-6_R8)
+ ASSERT(solar_declination == calendar_day * 2.0_R8)
+ ASSERT(earth_sun_distance == calendar_day * 3.0_R8)
+
+ end subroutine test_orbital_data_functions
+
+end program test_orbital_data
\ No newline at end of file
diff --git a/test/unit/physics/utils/test_orbital_data_stubs.F90 b/test/unit/physics/utils/test_orbital_data_stubs.F90
new file mode 100644
index 00000000..7822a140
--- /dev/null
+++ b/test/unit/physics/utils/test_orbital_data_stubs.F90
@@ -0,0 +1,65 @@
+! Copyright (C) 2024 National Science Foundation-National Center for Atmospheric Research
+! SPDX-License-Identifier: Apache-2.0
+!--------------------------------------------------------------------------
+!
+! Stub modules for the orbital_data module tests.
+!
+!--------------------------------------------------------------------------
+
+module shr_kind_mod
+ implicit none
+ integer, parameter :: SHR_KIND_R8 = selected_real_kind(15)
+end module shr_kind_mod
+module ccpp_kinds
+ implicit none
+ integer, parameter :: kind_phys = selected_real_kind(15)
+end module ccpp_kinds
+module cam_abortutils
+ implicit none
+ public :: check_allocate
+contains
+ subroutine check_allocate(error_code, subroutine_name, variable_name, file, line)
+ integer, intent(in) :: error_code
+ character(len=*), intent(in) :: subroutine_name
+ character(len=*), intent(in) :: variable_name
+ character(len=*), intent(in) :: file
+ integer, intent(in) :: line
+ if (error_code /= 0) then
+ write(*,*) "Allocation of '", variable_name, "' failed with code ", error_code, " in ", subroutine_name, " at ", file, ":", line
+ stop 3
+ end if
+ end subroutine check_allocate
+end module cam_abortutils
+module shr_orb_mod
+ use shr_kind_mod, only: R8 => SHR_KIND_R8
+ implicit none
+ real(kind=R8), parameter :: SHR_ORB_UNDEF_REAL = -1.0_R8
+ public :: shr_orb_decl, shr_orb_cosz
+contains
+ subroutine shr_orb_decl(calendar_day, eccen, mvelpp, lambm0, obliqr, solar_declination, earth_sun_distance)
+ real(kind=R8), intent(in) :: calendar_day
+ real(kind=R8), intent(in) :: eccen
+ real(kind=R8), intent(in) :: mvelpp
+ real(kind=R8), intent(in) :: lambm0
+ real(kind=R8), intent(in) :: obliqr
+ real(kind=R8), intent(out) :: solar_declination
+ real(kind=R8), intent(out) :: earth_sun_distance
+ solar_declination = calendar_day * 2.0
+ earth_sun_distance = calendar_day * 3.0
+ end subroutine shr_orb_decl
+ real(R8) pure function shr_orb_cosz(calendar_day, latitude, longitude, solar_declination)
+ real(kind=R8), intent(in) :: calendar_day
+ real(kind=R8), intent(in) :: latitude
+ real(kind=R8), intent(in) :: longitude
+ real(kind=R8), intent(in) :: solar_declination
+ shr_orb_cosz = latitude + longitude + solar_declination
+ end function shr_orb_cosz
+end module shr_orb_mod
+module cam_control_mod
+ use shr_kind_mod, only: R8 => SHR_KIND_R8
+ implicit none
+ real(kind=R8) :: eccen = 2.0_R8
+ real(kind=R8) :: mvelpp = 3.0_R8
+ real(kind=R8) :: lambm0 = 4.0_R8
+ real(kind=R8) :: obliqr = 5.0_R8
+end module cam_control_mod
\ No newline at end of file
diff --git a/test/valgrind.supp b/test/valgrind.supp
new file mode 100644
index 00000000..ee1ba85e
--- /dev/null
+++ b/test/valgrind.supp
@@ -0,0 +1,81 @@
+##############################################################
+#
+# MUSICA TUV-x suppressions
+#
+# TODO(jiwon) We are experiencing memory leak issues in certain
+# functions of TUV-x. It appears that these leaks occur only
+# occasionally during initialization. We believe it’s acceptable
+# to add a Valgrind suppression for now, and we will investigate
+# further if it becomes a significant concern.
+#
+##############################################################
+{
+ Suppress_MUSICA_TUV-x_Leak1
+ Memcheck:Leak
+ fun:malloc
+ fun:__musica_config_MOD_get_string
+ fun:__tuvx_radiator_aerosol_MOD_constructor
+ fun:__tuvx_radiator_factory_MOD_radiator_builder
+ fun:__tuvx_radiator_warehouse_MOD_constructor
+ fun:__tuvx_radiative_transfer_MOD_constructor
+ fun:__tuvx_core_MOD_constructor
+ fun:InternalCreateTuvx
+ ...
+}
+{
+ Suppress_MUSICA_TUV-x_Leak2
+ Memcheck:Leak
+ fun:malloc
+ fun:__musica_config_MOD_get_string
+ fun:__tuvx_radiator_MOD_base_constructor
+ fun:__tuvx_radiator_MOD_constructor
+ fun:__tuvx_radiator_factory_MOD_radiator_builder
+ fun:__tuvx_radiator_warehouse_MOD_constructor
+ fun:__tuvx_radiative_transfer_MOD_constructor
+ fun:__tuvx_core_MOD_constructor
+ fun:InternalCreateTuvx
+ ...
+}
+{
+ Suppress_MUSICA_TUV-x_CreateRadiator
+ Memcheck:Leak
+ match-leak-kinds: definite
+ fun:malloc
+ fun:__musica_string_MOD_string_assign_char
+ fun:__tuvx_radiator_from_host_MOD_constructor_char
+ fun:__tuvx_radiator_from_host_MOD_constructor_string
+ fun:InternalCreateRadiator
+ ...
+}
+{
+ Suppress_MUSICA_TUV-x_AddRadiator
+ Memcheck:Leak
+ match-leak-kinds: definite
+ fun:malloc
+ fun:__tuvx_radiator_from_host_MOD___copy_tuvx_radiator_from_host_Radiator_from_host_t
+ fun:__tuvx_radiator_warehouse_MOD_add_radiator
+ fun:InternalAddRadiator
+ ...
+}
+{
+ Suppress_MUSICA_TUV-x_GetRadiator
+ Memcheck:Leak
+ match-leak-kinds: definite
+ fun:malloc
+ fun:__tuvx_radiator_from_host_MOD___copy_tuvx_radiator_from_host_Radiator_from_host_t
+ fun:InternalGetRadiator
+ ...
+}
+{
+ Suppress_MUSICA_TUV-x_CreateTuvx-RadiatorFromHost
+ Memcheck:Leak
+ match-leak-kinds: definite
+ fun:malloc
+ fun:__tuvx_radiator_from_host_MOD___copy_tuvx_radiator_from_host_Radiator_from_host_t
+ fun:__tuvx_radiator_warehouse_MOD_add_radiator
+ fun:__tuvx_radiator_warehouse_MOD_add_radiators
+ fun:__tuvx_radiative_transfer_MOD_constructor
+ fun:__tuvx_core_MOD_constructor
+ fun:InternalCreateTuvx
+ ...
+}
\ No newline at end of file