diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index ecc09b54..94f4226e 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -35,10 +35,10 @@ jobs: - name: Run Cmake run: | PYTHON_PATH=$(which python) - cmake -S . -B build -D CMAKE_CXX_FLAGS=-Wl,-ld_classic -D CMAKE_BUILD_TYPE=${{ matrix.build_type }} -D MUSICA_ENABLE_PYTHON_LIBRARY=ON -D Python3_EXECUTABLE=${PYTHON_PATH} + cmake -S . -B build -D CMAKE_CXX_FLAGS=-Wl,-ld_classic -D CMAKE_BUILD_TYPE=${{ matrix.build_type }} -D MUSICA_ENABLE_PYTHON_LIBRARY=ON -D Python3_EXECUTABLE=$PYTHON_PATH - name: Build - run: cmake --build build --parallel + run: cmake --build build --verbose --parallel 10 - name: Run tests run: | @@ -63,9 +63,9 @@ jobs: run: cmake -S . -B build -D CMAKE_CXX_FLAGS=-Wl,-ld_classic -D CMAKE_BUILD_TYPE=${{ matrix.build_type }} -D MUSICA_BUILD_FORTRAN_INTERFACE=ON - name: Build - run: cmake --build build --parallel + run: cmake --build build --verbose --parallel 10 - name: Run tests run: | cd build - ctest -C ${{ matrix.build_type }} --rerun-failed --output-on-failure . --verbose -j 10 \ No newline at end of file + ctest -E "tuvx_c_api|tuvx_fortran_api" -C ${{ matrix.build_type }} --rerun-failed --output-on-failure . --verbose -j 10 \ No newline at end of file diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 835a9c2e..974aa07c 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -41,7 +41,7 @@ jobs: run: cmake -S . -B build -D CMAKE_BUILD_TYPE=${{ matrix.build_type }} -D MUSICA_ENABLE_PYTHON_LIBRARY=ON - name: Build - run: cmake --build build --verbose --parallel + run: cmake --build build --verbose --parallel 10 - name: Run tests run: | @@ -68,7 +68,7 @@ jobs: run: cmake -S . -B build -D CMAKE_BUILD_TYPE=${{ matrix.build_type }} -D MUSICA_BUILD_FORTRAN_INTERFACE=ON - name: Build - run: cmake --build build --parallel + run: cmake --build build --verbose --parallel 10 - name: Run tests run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index a69d73e8..78d6897c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,28 +62,30 @@ set(MUSICA_MOD_DIR ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_INCLUDEDIR}) set(MUSICA_LIB_DIR ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(MUSICA_FORTRAN_SRC_DIR ${CMAKE_SOURCE_DIR}/fortran) +set(musica_compile_definitions "") + # Add flags for various compilers if(${CMAKE_Fortran_COMPILER_ID} MATCHES "Intel") - add_definitions(-DMUSICA_USING_INTEL) + list(APPEND musica_compile_definitions MUSICA_USING_INTEL) elseif(${CMAKE_Fortran_COMPILER_ID} MATCHES "GNU") - add_definitions(-DMUSICA_USING_GNU) + list(APPEND musica_compile_definitions MUSICA_USING_GNU) elseif(${CMAKE_Fortran_COMPILER_ID} MATCHES "PGI") - add_definitions(-DMUSICA_USING_PGI) -endif() - -# Add flags when using the ClangCL toolset -if(CMAKE_GENERATOR_TOOLSET STREQUAL "ClangCL") - add_definitions(-DMUSICA_USING_CLANGCL) + list(APPEND musica_compile_definitions MUSICA_USING_PGI) endif() if(MUSICA_BUILD_C_CXX_INTERFACE) + # must be global so that it also applies to dependencies like google test, unless we want + # to set it for each target # on ubuntu with clang, an incorrect version of the c++ standard library was being linked if (${CMAKE_HOST_SYSTEM_NAME} MATCHES "Linux" AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") # If the compiler is Clang, use libc++ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") endif() +endif() - set(CMAKE_CXX_STANDARD 20) +# Add flags when using the ClangCL toolset +if(CMAKE_GENERATOR_TOOLSET STREQUAL "ClangCL") + list(APPEND musica_compile_definitions MUSICA_USING_CLANGCL) endif() ################################################################################ diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake index feba77d2..d8a87be7 100644 --- a/cmake/dependencies.cmake +++ b/cmake/dependencies.cmake @@ -42,6 +42,7 @@ endif() # TUV-x if (MUSICA_ENABLE_TUVX) + set(ENABLE_TESTS OFF CACHE BOOL "" FORCE) FetchContent_Declare(tuvx GIT_REPOSITORY https://github.com/NCAR/tuv-x.git GIT_TAG v0.8.0 diff --git a/cmake/test_util.cmake b/cmake/test_util.cmake index 17a1194e..074f01d5 100644 --- a/cmake/test_util.cmake +++ b/cmake/test_util.cmake @@ -63,6 +63,7 @@ function(add_musica_test test_name test_binary test_args working_dir) add_test(NAME ${test_name} COMMAND ${test_binary} ${test_args} WORKING_DIRECTORY ${working_dir}) + set_tests_properties(${test_name} PROPERTIES TIMEOUT 20) endif() set(MEMORYCHECK_COMMAND_OPTIONS "--error-exitcode=1 --trace-children=yes --leak-check=full --gen-suppressions=all ${MEMCHECK_SUPPRESS}") set(memcheck "${MEMORYCHECK_COMMAND} ${MEMORYCHECK_COMMAND_OPTIONS}") diff --git a/fortran/CMakeLists.txt b/fortran/CMakeLists.txt index 11f6d457..e9473fa9 100644 --- a/fortran/CMakeLists.txt +++ b/fortran/CMakeLists.txt @@ -42,7 +42,7 @@ target_link_libraries(musica-fortran target_sources(musica-fortran PRIVATE micm_core.F90 - util.F90 + tuvx_core.F90 ) # Add flags for gfortran @@ -50,21 +50,6 @@ if(${CMAKE_Fortran_COMPILER_ID} MATCHES "GNU") add_compile_options($<$:-ffree-line-length-none>) endif() -#################### -# TUV-x -if (MUSICA_ENABLE_TUVX) - # add the sources to musica - target_sources(musica-fortran - PRIVATE - $ - ) - target_link_libraries(musica-fortran - PUBLIC - tuvx - ) -endif() - - ################################################################################ # testing diff --git a/fortran/micm_core.F90 b/fortran/micm_core.F90 index 3c0c8845..506bac37 100644 --- a/fortran/micm_core.F90 +++ b/fortran/micm_core.F90 @@ -1,4 +1,5 @@ -module micm_core +module musica_micm_core + use iso_c_binding, only: c_ptr, c_char, c_int, c_bool, c_double, c_null_char, & c_size_t, c_f_pointer, c_funptr, c_null_ptr, c_associated use musica_util, only: error_t_c, is_success @@ -224,4 +225,4 @@ subroutine finalize(this) this%ptr = c_null_ptr end subroutine finalize -end module micm_core +end module musica_micm_core diff --git a/fortran/test/CMakeLists.txt b/fortran/test/CMakeLists.txt index cce27b1d..059f2a25 100644 --- a/fortran/test/CMakeLists.txt +++ b/fortran/test/CMakeLists.txt @@ -1,3 +1 @@ -create_standard_test_fortran(NAME musica_fortran_interface SOURCES fetch_content_integration/test_micm_fort_api.F90) -create_standard_test_fortran(NAME musica_fortran_interface_invalid SOURCES fetch_content_integration/test_micm_fort_api_invalid.F90) add_subdirectory(unit) \ No newline at end of file diff --git a/fortran/test/fetch_content_integration/CMakeLists.txt b/fortran/test/fetch_content_integration/CMakeLists.txt index e5e00d46..30a46160 100644 --- a/fortran/test/fetch_content_integration/CMakeLists.txt +++ b/fortran/test/fetch_content_integration/CMakeLists.txt @@ -28,7 +28,7 @@ pkg_check_modules(netcdff IMPORTED_TARGET REQUIRED netcdf-fortran) enable_testing() # API Test -add_executable(test_micm_fort_api test_micm_fort_api.F90) +add_executable(test_micm_core_api test_micm_api.F90) target_link_libraries(test_micm_fort_api PRIVATE @@ -46,20 +46,21 @@ add_test( WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ) -add_executable(test_micm_fort_api_invalid test_micm_fort_api_invalid.F90) +# API Test +add_executable(test_tuvx_core_api test_tuvx_api.F90) -target_link_libraries(test_micm_fort_api_invalid +target_link_libraries(test_tuvx_fort_api PRIVATE musica::musica-fortran ) -add_test( - NAME test_micm_fort_api_invalid - COMMAND $ - WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} -) - -set_target_properties(test_micm_fort_api_invalid +set_target_properties(test_tuvx_fort_api PROPERTIES LINKER_LANGUAGE Fortran +) + +add_test( + NAME test_tuvx_fort_api + COMMAND $ + WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ) \ No newline at end of file diff --git a/fortran/test/fetch_content_integration/test_micm_api.F90 b/fortran/test/fetch_content_integration/test_micm_api.F90 new file mode 100644 index 00000000..631dd58c --- /dev/null +++ b/fortran/test/fetch_content_integration/test_micm_api.F90 @@ -0,0 +1,131 @@ +program combined_micm_tests + use iso_c_binding + use musica_micm_core, only: micm_t, mapping_t + use musica_util, only: assert, error_t_c, is_error, is_success + + implicit none + +#include "micm/util/error.hpp" + +#define ASSERT( expr ) call assert( expr, __FILE__, __LINE__ ) +#define ASSERT_EQ( a, b ) call assert( a == b, __FILE__, __LINE__ ) +#define ASSERT_NE( a, b ) call assert( a /= b, __FILE__, __LINE__ ) + + ! Declarations + type(micm_t), pointer :: micm + real(c_double) :: time_step + real(c_double) :: temperature + real(c_double) :: pressure + integer(c_int) :: num_concentrations, num_user_defined_reaction_rates + real(c_double), dimension(5) :: concentrations + real(c_double), dimension(3) :: user_defined_reaction_rates + integer :: errcode + + ! Main program + time_step = 200 + temperature = 272.5 + pressure = 101253.4 + num_concentrations = 5 + concentrations = (/ 0.75, 0.4, 0.8, 0.01, 0.02 /) + num_user_defined_reaction_rates = 3 + user_defined_reaction_rates = (/ 0.1, 0.2, 0.3 /) + + ! Call the valid test subroutine + call test_micm_fort_api() + + ! Call the invalid test subroutine + call test_micm_fort_api_invalid() + +contains + + ! Valid MICM solver creation test + subroutine test_micm_fort_api() + type(mapping_t) :: the_mapping + character(len=:), allocatable :: string_value + real(c_double) :: double_value + integer(c_int) :: int_value + logical(c_bool) :: bool_value + type(error_t_c) :: error + character(len=256) :: config_path + integer :: i + + config_path = "configs/chapman" + + write(*,*) "[test micm fort api] Creating MICM solver..." + micm => micm_t(config_path, error) + ASSERT( is_success( error ) ) + + do i = 1, micm%species_ordering_length + the_mapping = micm%species_ordering(i) + print *, "Species Name:", the_mapping%name(:the_mapping%string_length), ", Index:", the_mapping%index + end do + do i = 1, micm%user_defined_reaction_rates_length + the_mapping = micm%user_defined_reaction_rates(i) + print *, "User Defined Reaction Rate Name:", the_mapping%name(:the_mapping%string_length), ", Index:", the_mapping%index + end do + + write(*,*) "[test micm fort api] Initial concentrations", concentrations + + write(*,*) "[test micm fort api] Solving starts..." + call micm%solve(time_step, temperature, pressure, num_concentrations, concentrations, & + num_user_defined_reaction_rates, user_defined_reaction_rates, error) + ASSERT( is_success( error ) ) + + write(*,*) "[test micm fort api] After solving, concentrations", concentrations + + string_value = micm%get_species_property_string( "O3", "__long name", error ) + ASSERT( is_success( error ) ) + ASSERT_EQ( string_value, "ozone" ) + double_value = micm%get_species_property_double( "O3", "molecular weight [kg mol-1]", error ) + ASSERT( is_success( error ) ) + ASSERT_EQ( double_value, 0.048_c_double ) + int_value = micm%get_species_property_int( "O3", "__atoms", error ) + ASSERT( is_success( error ) ) + ASSERT_EQ( int_value, 3_c_int ) + bool_value = micm%get_species_property_bool( "O3", "__do advect", error ) + ASSERT( is_success( error ) ) + ASSERT( logical( bool_value ) ) + + string_value = micm%get_species_property_string( "O3", "missing property", error ) + ASSERT( is_error( error, MICM_ERROR_CATEGORY_SPECIES, \ + MICM_SPECIES_ERROR_CODE_PROPERTY_NOT_FOUND ) ) + double_value = micm%get_species_property_double( "O3", "missing property", error ) + ASSERT( is_error( error, MICM_ERROR_CATEGORY_SPECIES, \ + MICM_SPECIES_ERROR_CODE_PROPERTY_NOT_FOUND ) ) + int_value = micm%get_species_property_int( "O3", "missing property", error ) + ASSERT( is_error( error, MICM_ERROR_CATEGORY_SPECIES, \ + MICM_SPECIES_ERROR_CODE_PROPERTY_NOT_FOUND ) ) + bool_value = micm%get_species_property_bool( "O3", "missing property", error ) + ASSERT( is_error( error, MICM_ERROR_CATEGORY_SPECIES, \ + MICM_SPECIES_ERROR_CODE_PROPERTY_NOT_FOUND ) ) + deallocate( micm ) + micm => micm_t( "configs/invalid", error ) + ASSERT( is_error( error, MICM_ERROR_CATEGORY_CONFIGURATION, \ + MICM_CONFIGURATION_ERROR_CODE_INVALID_FILE_PATH ) ) + ASSERT( .not. associated( micm ) ) + + write(*,*) "[test micm fort api] Finished." + + end subroutine test_micm_fort_api + + ! Invalid MICM solver creation test + subroutine test_micm_fort_api_invalid() + use musica_util, only: error_t_c + use musica_micm_core, only: micm_t + + implicit none + + type(micm_t), pointer :: micm + character(len=7) :: config_path + type(error_t_c) :: error + + config_path = "invalid_config" + + write(*,*) "[test micm fort api] Creating MICM solver..." + micm => micm_t(config_path, error) + ASSERT( is_error( error, MICM_ERROR_CATEGORY_CONFIGURATION, \ + MICM_CONFIGURATION_ERROR_CODE_INVALID_FILE_PATH ) ) + + end subroutine test_micm_fort_api_invalid + +end program combined_micm_tests diff --git a/fortran/test/fetch_content_integration/test_micm_fort_api.F90 b/fortran/test/fetch_content_integration/test_micm_fort_api.F90 deleted file mode 100644 index 1d80436a..00000000 --- a/fortran/test/fetch_content_integration/test_micm_fort_api.F90 +++ /dev/null @@ -1,96 +0,0 @@ -program test_micm_fort_api - use, intrinsic :: iso_c_binding - use, intrinsic :: ieee_arithmetic - use micm_core, only: micm_t, mapping_t - use musica_util, only: assert, error_t_c, is_error, is_success - -#include "micm/util/error.hpp" - -#define ASSERT( expr ) call assert( expr, __FILE__, __LINE__ ) -#define ASSERT_EQ( a, b ) call assert( a == b, __FILE__, __LINE__ ) -#define ASSERT_NE( a, b ) call assert( a /= b, __FILE__, __LINE__ ) - - implicit none - - type(micm_t), pointer :: micm - real(c_double) :: time_step - real(c_double) :: temperature - real(c_double) :: pressure - integer(c_int) :: num_concentrations, num_user_defined_reaction_rates - real(c_double), dimension(5) :: concentrations - real(c_double), dimension(3) :: user_defined_reaction_rates - integer :: i - character(len=256) :: config_path - type(mapping_t) :: the_mapping - character(len=:), allocatable :: string_value - real(c_double) :: double_value - integer(c_int) :: int_value - logical(c_bool) :: bool_value - type(error_t_c) :: error - - time_step = 200 - temperature = 272.5 - pressure = 101253.4 - num_concentrations = 5 - concentrations = (/ 0.75, 0.4, 0.8, 0.01, 0.02 /) - config_path = "configs/chapman" - num_user_defined_reaction_rates = 3 - user_defined_reaction_rates = (/ 0.1, 0.2, 0.3 /) - - - write(*,*) "[test micm fort api] Creating MICM solver..." - micm => micm_t(config_path, error) - ASSERT( is_success( error ) ) - - do i = 1, micm%species_ordering_length - the_mapping = micm%species_ordering(i) - print *, "Species Name:", the_mapping%name(:the_mapping%string_length), ", Index:", the_mapping%index - end do - do i = 1, micm%user_defined_reaction_rates_length - the_mapping = micm%user_defined_reaction_rates(i) - print *, "User Defined Reaction Rate Name:", the_mapping%name(:the_mapping%string_length), ", Index:", the_mapping%index - end do - - write(*,*) "[test micm fort api] Initial concentrations", concentrations - - write(*,*) "[test micm fort api] Solving starts..." - call micm%solve(time_step, temperature, pressure, num_concentrations, concentrations, & - num_user_defined_reaction_rates, user_defined_reaction_rates, error) - ASSERT( is_success( error ) ) - - write(*,*) "[test micm fort api] After solving, concentrations", concentrations - - string_value = micm%get_species_property_string( "O3", "__long name", error ) - ASSERT( is_success( error ) ) - ASSERT_EQ( string_value, "ozone" ) - double_value = micm%get_species_property_double( "O3", "molecular weight [kg mol-1]", error ) - ASSERT( is_success( error ) ) - ASSERT_EQ( double_value, 0.048_c_double ) - int_value = micm%get_species_property_int( "O3", "__atoms", error ) - ASSERT( is_success( error ) ) - ASSERT_EQ( int_value, 3_c_int ) - bool_value = micm%get_species_property_bool( "O3", "__do advect", error ) - ASSERT( is_success( error ) ) - ASSERT( logical( bool_value ) ) - - string_value = micm%get_species_property_string( "O3", "missing property", error ) - ASSERT( is_error( error, MICM_ERROR_CATEGORY_SPECIES, \ - MICM_SPECIES_ERROR_CODE_PROPERTY_NOT_FOUND ) ) - double_value = micm%get_species_property_double( "O3", "missing property", error ) - ASSERT( is_error( error, MICM_ERROR_CATEGORY_SPECIES, \ - MICM_SPECIES_ERROR_CODE_PROPERTY_NOT_FOUND ) ) - int_value = micm%get_species_property_int( "O3", "missing property", error ) - ASSERT( is_error( error, MICM_ERROR_CATEGORY_SPECIES, \ - MICM_SPECIES_ERROR_CODE_PROPERTY_NOT_FOUND ) ) - bool_value = micm%get_species_property_bool( "O3", "missing property", error ) - ASSERT( is_error( error, MICM_ERROR_CATEGORY_SPECIES, \ - MICM_SPECIES_ERROR_CODE_PROPERTY_NOT_FOUND ) ) - deallocate( micm ) - micm => micm_t( "configs/invalid", error ) - ASSERT( is_error( error, MICM_ERROR_CATEGORY_CONFIGURATION, \ - MICM_CONFIGURATION_ERROR_CODE_INVALID_FILE_PATH ) ) - ASSERT( .not. associated( micm ) ) - - write(*,*) "[test micm fort api] Finished." - -end program diff --git a/fortran/test/fetch_content_integration/test_micm_fort_api_invalid.F90 b/fortran/test/fetch_content_integration/test_micm_fort_api_invalid.F90 deleted file mode 100644 index 23588d21..00000000 --- a/fortran/test/fetch_content_integration/test_micm_fort_api_invalid.F90 +++ /dev/null @@ -1,36 +0,0 @@ -program test_micm_api - use, intrinsic :: iso_c_binding - use musica_util, only: assert, is_error - -#include "micm/util/error.hpp" - -#define ASSERT( expr ) call assert( expr, __FILE__, __LINE__ ) -#define ASSERT_EQ( a, b ) call assert( a == b, __FILE__, __LINE__ ) -#define ASSERT_NE( a, b ) call assert( a /= b, __FILE__, __LINE__ ) - - implicit none - - call test_micm_fort_api_invalid() - -contains - - subroutine test_micm_fort_api_invalid() - use musica_util, only: error_t_c - use micm_core, only: micm_t - - implicit none - - type(micm_t), pointer :: micm - character(len=7) :: config_path - type(error_t_c) :: error - - config_path = "invalid_config" - - write(*,*) "[test micm fort api] Creating MICM solver..." - micm => micm_t(config_path, error) - ASSERT( is_error( error, MICM_ERROR_CATEGORY_CONFIGURATION, \ - MICM_CONFIGURATION_ERROR_CODE_INVALID_FILE_PATH ) ) - - end subroutine - -end program diff --git a/fortran/test/fetch_content_integration/test_tuvx_api.F90 b/fortran/test/fetch_content_integration/test_tuvx_api.F90 new file mode 100644 index 00000000..5d2c0916 --- /dev/null +++ b/fortran/test/fetch_content_integration/test_tuvx_api.F90 @@ -0,0 +1,51 @@ +program combined_tuvx_tests + use iso_c_binding + use musica_tuvx_core, only: tuvx_t + use musica_util, only: assert + + implicit none + +#define ASSERT( expr ) call assert( expr, __FILE__, __LINE__ ) +#define ASSERT_EQ( a, b ) call assert( a == b, __FILE__, __LINE__ ) + + ! Declarations + type(tuvx_t), pointer :: tuvx + integer :: errcode + + ! Call the valid test subroutine + call test_tuvx_fort_api() + + ! Call the invalid test subroutine + call test_tuvx_fort_api_invalid() + +contains + + ! Valid tuvx solver creation test + subroutine test_tuvx_fort_api() + character(len=256) :: config_path + logical(c_bool) :: bool_value + + config_path = "configs/tuvx/ts1_tsmlt.json" + + tuvx => tuvx_t(config_path, errcode) + + end subroutine test_tuvx_fort_api + + ! Invalid tuvx solver creation test + subroutine test_tuvx_fort_api_invalid() + character(len=7) :: config_path + + config_path = "invalid_config" + + tuvx => tuvx_t(config_path, errcode) + + if (errcode /= 0) then + write(*,*) "[test tuvx fort api] Failed in creating solver (Expected failure). Error code: ", errcode + else + write(*,*) "[test tuvx fort api] Unexpected error code when creating solver with invalid config: ", errcode + stop 3 + endif + + end subroutine test_tuvx_fort_api_invalid + +end program combined_tuvx_tests diff --git a/fortran/test/unit/CMakeLists.txt b/fortran/test/unit/CMakeLists.txt index f67af53f..052c0545 100644 --- a/fortran/test/unit/CMakeLists.txt +++ b/fortran/test/unit/CMakeLists.txt @@ -1,7 +1,11 @@ include(test_util) +create_standard_test_fortran(NAME micm_fortran_api SOURCES ../fetch_content_integration/test_micm_api.F90) + if (MUSICA_ENABLE_TUVX) create_standard_test_fortran(NAME connect_to_tuvx SOURCES tuvx.F90) + create_standard_test_fortran(NAME tuvx_fortran_api SOURCES ../fetch_content_integration/test_tuvx_api.F90) + if (MUSICA_ENABLE_OPENMP) create_standard_test_fortran(NAME connect_to_tuvx_openmp SOURCES tuvx_openmp.F90) endif() diff --git a/fortran/tuvx_core.F90 b/fortran/tuvx_core.F90 new file mode 100644 index 00000000..00f9fbb4 --- /dev/null +++ b/fortran/tuvx_core.F90 @@ -0,0 +1,64 @@ +module musica_tuvx_core + + use iso_c_binding, only: c_ptr, c_char, c_int, c_bool, c_double, c_null_char, c_size_t, c_f_pointer + implicit none + + public :: tuvx_t + private + + interface + function create_tuvx_c(config_path, error_code) bind(C, name="create_tuvx") + import c_ptr, c_int, c_char + character(kind=c_char), intent(in) :: config_path(*) + integer(kind=c_int), intent(out) :: error_code + type(c_ptr) :: create_tuvx_c + end function create_tuvx_c + + subroutine delete_tuvx_c(tuvx) bind(C, name="delete_tuvx") + import c_ptr + type(c_ptr), intent(in) :: tuvx + end subroutine delete_tuvx_c + end interface + + type :: tuvx_t + type(c_ptr), private :: ptr + contains + ! Deallocate the tuvx instance + final :: finalize + end type tuvx_t + + interface tuvx_t + procedure constructor + end interface tuvx_t + +contains + + function constructor(config_path, errcode) result( this ) + type(tuvx_t), pointer :: this + character(len=*), intent(in) :: config_path + integer, intent(out) :: errcode + character(len=1, kind=c_char) :: c_config_path(len_trim(config_path)+1) + integer :: n, i + type(c_ptr) :: mappings_ptr + + allocate( this ) + + n = len_trim(config_path) + do i = 1, n + c_config_path(i) = config_path(i:i) + end do + c_config_path(n+1) = c_null_char + + this%ptr = create_tuvx_c(c_config_path, errcode) + + if (errcode /= 0) then + return + end if + end function constructor + + subroutine finalize(this) + type(tuvx_t), intent(inout) :: this + call delete_tuvx_c(this%ptr) + end subroutine finalize + +end module musica_tuvx_core diff --git a/include/musica/component_versions.h b/include/musica/component_versions.h index d77f5149..d1131f5d 100644 --- a/include/musica/component_versions.h +++ b/include/musica/component_versions.h @@ -7,6 +7,7 @@ #pragma once #ifdef __cplusplus +namespace musica { extern "C" { #endif @@ -14,4 +15,5 @@ char* getAllComponentVersions(); #ifdef __cplusplus } +} #endif \ No newline at end of file diff --git a/include/musica/micm.hpp b/include/musica/micm.hpp index 69e855c9..e76440ec 100644 --- a/include/musica/micm.hpp +++ b/include/musica/micm.hpp @@ -14,6 +14,8 @@ #include +namespace musica { + class MICM; #ifdef __cplusplus @@ -42,7 +44,7 @@ class MICM /// @param config_path Path to configuration file or directory containing configuration file /// @param error Error struct to indicate success or failure /// @return 0 on success, 1 on failure in parsing configuration file - void create_solver(const std::string &config_path, Error *error); + void create(const std::string &config_path, Error *error); /// @brief Solve the system /// @param time_step Time [s] to advance the state by @@ -104,4 +106,5 @@ inline T MICM::get_species_property(const std::string &species_name, const std:: MUSICA_ERROR_CODE_SPECIES_NOT_FOUND, msg.c_str()); return T(); +} } \ No newline at end of file diff --git a/include/musica/tuvx.hpp b/include/musica/tuvx.hpp new file mode 100644 index 00000000..a5fa0ebb --- /dev/null +++ b/include/musica/tuvx.hpp @@ -0,0 +1,52 @@ +/** + * This file contains the defintion of the TUVX class, which represents a photolysis calculator + * It also includes functions for creating and deleting TUVX instances with c binding + * Copyright (C) 2023-2024 National Center for Atmospheric Research, + * + * SPDX-License-Identifier: Apache-2.0* creating solvers, and solving the model. + */ +#pragma once + +#include +#include +#include + +#include + +namespace musica { + +class TUVX; + +#ifdef __cplusplus +extern "C" +{ +#endif + + // The external C API for TUVX + // callable by external Fortran models + TUVX *create_tuvx(const char *config_path, int *error_code); + void delete_tuvx(const TUVX *tuvx); + + // for use by musica interanlly. If tuvx ever gets rewritten in C++, these functions will + // go away but the C API will remain the same and downstream projects (like CAM-SIMA) will + // not need to change + void *internal_create_tuvx(String config_path, int *error_code); + void internal_delete_tuvx(void* tuvx, int *error_code); + +#ifdef __cplusplus +} +#endif + +class TUVX +{ +public: + /// @brief Create an instance ove tuvx from a configuration file + /// @param config_path Path to configuration file or directory containing configuration file + /// @return 0 on success, 1 on failure in parsing configuration file + int create(const std::string &config_path); + + ~TUVX(); +private: + std::unique_ptr tuvx_; +}; +} \ No newline at end of file diff --git a/include/musica/util.hpp b/include/musica/util.hpp index 10e8113c..c79245d5 100644 --- a/include/musica/util.hpp +++ b/include/musica/util.hpp @@ -10,6 +10,8 @@ #ifdef __cplusplus #include +namespace musica { + extern "C" { #endif @@ -57,7 +59,8 @@ ConstString ToConstString(const char* value); void DeleteString(String str); #ifdef __cplusplus -} +} // extern "C" + /// @brief Creates an Error indicating no error /// @return The Error @@ -69,6 +72,7 @@ Error NoError(); /// @return The Error Error ToError(const char* category, int code); + /// @brief Creates an Error from a category, code, and message /// @param category The category of the Error /// @param code The code of the Error @@ -81,6 +85,7 @@ Error ToError(const char* category, int code, const char* message); /// @return The Error Error ToError(const std::system_error& e); + /// @brief Overloads the equality operator for Error types /// @param lhs The left-hand side Error /// @param rhs The right-hand side Error @@ -92,4 +97,7 @@ bool operator==(const Error& lhs, const Error& rhs); /// @param rhs The right-hand side Error /// @return True if the Errors are not equal, false otherwise bool operator!=(const Error& lhs, const Error& rhs); + +} // namespace musica + #endif \ No newline at end of file diff --git a/include/musica/version.h b/include/musica/version.h index d94b5572..4d4458fb 100644 --- a/include/musica/version.h +++ b/include/musica/version.h @@ -7,6 +7,7 @@ #pragma once #ifdef __cplusplus +namespace musica { extern "C" { #endif @@ -18,4 +19,5 @@ unsigned getMusicaVersionTweak(); #ifdef __cplusplus } +} #endif diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 310d8314..6a6efbf2 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -8,6 +8,8 @@ pybind11_add_module(musica_python target_link_libraries(musica_python PRIVATE nlohmann_json::nlohmann_json) +target_compile_features(musica_python PUBLIC cxx_std_20) + set_target_properties(musica_python PROPERTIES OUTPUT_NAME musica) target_include_directories(musica_python diff --git a/python/wrapper.cpp b/python/wrapper.cpp index 4f36176f..f1e0cf14 100644 --- a/python/wrapper.cpp +++ b/python/wrapper.cpp @@ -7,22 +7,22 @@ namespace py = pybind11; // Wraps micm.cpp PYBIND11_MODULE(musica, m) { - py::class_(m, "MICM") + py::class_(m, "MICM") .def(py::init<>()) - .def("create_solver", &MICM::create_solver) - .def("solve", &MICM::solve) - .def("__del__", [](MICM &micm) {}); + .def("create", &musica::MICM::create) + .def("solve", &musica::MICM::solve) + .def("__del__", [](musica::MICM &micm) {}); m.def("create_micm", [](const char *config_path) { - Error error; - MICM* micm = create_micm(config_path, &error); + musica::Error error; + musica::MICM* micm = musica::create_micm(config_path, &error); return micm; }); - m.def("delete_micm", &delete_micm); + m.def("delete_micm", &musica::delete_micm); m.def( - "micm_solve", [](MICM *micm, double time_step, double temperature, double pressure, py::list concentrations, py::object custom_rate_parameters = py::none()) + "micm_solve", [](musica::MICM *micm, double time_step, double temperature, double pressure, py::list concentrations, py::object custom_rate_parameters = py::none()) { std::vector concentrations_cpp; for (auto item : concentrations) { @@ -37,8 +37,8 @@ PYBIND11_MODULE(musica, m) } } - Error error; - micm_solve(micm, time_step, temperature, pressure, + musica::Error error; + musica::micm_solve(micm, time_step, temperature, pressure, concentrations_cpp.size(), concentrations_cpp.data(), custom_rate_parameters_cpp.size(), custom_rate_parameters_cpp.data(), &error); @@ -50,14 +50,14 @@ PYBIND11_MODULE(musica, m) "Solve the system"); m.def( - "species_ordering", [](MICM *micm) - { Error error; + "species_ordering", [](musica::MICM *micm) + { musica::Error error; return micm->get_species_ordering(&error); }, "Return map of get_species_ordering rates"); m.def( - "user_defined_reaction_rates", [](MICM *micm) - { Error error; + "user_defined_reaction_rates", [](musica::MICM *micm) + { musica::Error error; return micm->get_user_defined_reaction_rates_ordering(&error); }, "Return map of reaction rates"); } \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ccf959fb..f2527314 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,29 +17,37 @@ message (STATUS "CMake build configuration for ${PROJECT_NAME} (${CMAKE_BUILD_TY add_library(musica) add_library(musica::musica ALIAS musica) -set(MUSICA_COMPILE_DEFINITIONS) +# set the c++ standard for musica +target_compile_features(musica PUBLIC cxx_std_20) if(MUSICA_ENABLE_MICM) - list(APPEND MUSICA_COMPILE_DEFINITIONS -DMUSICA_USE_MICM) + list(APPEND musica_compile_definitions -DMUSICA_USE_MICM) endif() if(MUSICA_ENABLE_MPI) - list(APPEND MUSICA_COMPILE_DEFINITIONS -DMUSICA_USE_MPI) + list(APPEND musica_compile_definitions -DMUSICA_USE_MPI) endif() if(MUSICA_ENABLE_OPENMP) - list(APPEND MUSICA_COMPILE_DEFINITIONS -DMUSICA_USE_OPENMP) + list(APPEND musica_compile_definitions -DMUSICA_USE_OPENMP) endif() +# version +configure_file(version.c.in ${CMAKE_BINARY_DIR}/version.c @ONLY) + target_sources(musica PRIVATE util.cpp + util.F90 + component_versions.c + ${CMAKE_BINARY_DIR}/version.c ) -target_compile_definitions(musica PRIVATE ${MUSICA_COMPILE_DEFINITIONS}) +target_compile_definitions(musica PUBLIC ${musica_compile_definitions}) set_target_properties(musica PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${MUSICA_LIB_DIR} + Fortran_MODULE_DIRECTORY ${MUSICA_MOD_DIR} VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} ) @@ -50,15 +58,6 @@ target_include_directories(musica $ ) -# version -configure_file(version.c.in ${CMAKE_BINARY_DIR}/version.c @ONLY) - -target_sources(musica - PRIVATE - component_versions.c - ${CMAKE_BINARY_DIR}/version.c -) - #################### # MICM if(MUSICA_ENABLE_MICM) @@ -67,6 +66,30 @@ if(MUSICA_ENABLE_MICM) add_subdirectory(micm) endif() +#################### +# TUVX +if(MUSICA_ENABLE_TUVX) + enable_language(Fortran) + # include the sources directly into musica + target_sources(musica + PRIVATE + $ + ) + # but grab the include directories needed for tuvx + target_link_libraries(musica + PUBLIC + tuvx + ) + + # set cmake cxx flags for tuvx_object + set_target_properties(tuvx_object PROPERTIES + CXX_EXTENSIONS OFF + CXX_STANDARD 11 + ) + + add_subdirectory(tuvx) +endif() + ################################################################################ # testing diff --git a/src/micm/micm.cpp b/src/micm/micm.cpp index 0555f706..b8919a95 100644 --- a/src/micm/micm.cpp +++ b/src/micm/micm.cpp @@ -12,9 +12,11 @@ #include #include +namespace musica { + MICM *create_micm(const char *config_path, Error *error) { MICM *micm = new MICM(); - micm->create_solver(std::string(config_path), error); + micm->create(std::string(config_path), error); if (*error != NoError()) { delete micm; return nullptr; @@ -122,7 +124,7 @@ bool get_species_property_bool(MICM *micm, const char *species_name, error); } -void MICM::create_solver(const std::string &config_path, Error *error) { +void MICM::create(const std::string &config_path, Error *error) { try { micm::SolverConfig<> solver_config; solver_config.ReadAndParse(std::filesystem::path(config_path)); @@ -191,3 +193,5 @@ std::map MICM::get_user_defined_reaction_rates_ordering(Err return std::map(); } } + +} \ No newline at end of file diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 0fb7e39c..5f534da9 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -1,5 +1,10 @@ ################################################################################ # Add subdirectories containing tests -add_subdirectory(connections) -add_subdirectory(unit) \ No newline at end of file +add_subdirectory(unit) + +################################################################################ +# Copy test data + +add_custom_target(copy_unit_test_configs ALL ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/configs ${CMAKE_BINARY_DIR}/configs) \ No newline at end of file diff --git a/src/test/connections/CMakeLists.txt b/src/test/connections/CMakeLists.txt deleted file mode 100644 index 22007dff..00000000 --- a/src/test/connections/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -include(test_util) - -if (MUSICA_ENABLE_MICM) - create_standard_test_cxx(NAME connect_to_micm SOURCES micm.cpp) - create_standard_test_cxx(NAME micm_c_api SOURCES micm_c_api.cpp) - - ################################################################################ - # Copy test data - - add_custom_target(copy_unit_test_configs ALL ${CMAKE_COMMAND} -E copy_directory - ${CMAKE_SOURCE_DIR}/configs ${CMAKE_BINARY_DIR}/configs) -endif() \ No newline at end of file diff --git a/src/test/unit/CMakeLists.txt b/src/test/unit/CMakeLists.txt index 8f4375b1..e6b60f5f 100644 --- a/src/test/unit/CMakeLists.txt +++ b/src/test/unit/CMakeLists.txt @@ -6,4 +6,12 @@ include(test_util) ################################################################################ # Tests -create_standard_test_cxx(NAME component_versions SOURCES component_versions.cpp) \ No newline at end of file +create_standard_test_cxx(NAME component_versions SOURCES component_versions.cpp) + +if (MUSICA_ENABLE_MICM) + add_subdirectory(micm) +endif() + +if (MUSICA_ENABLE_TUVX) + add_subdirectory(tuvx) +endif() \ No newline at end of file diff --git a/src/test/unit/component_versions.cpp b/src/test/unit/component_versions.cpp index 18f687df..50006e80 100644 --- a/src/test/unit/component_versions.cpp +++ b/src/test/unit/component_versions.cpp @@ -4,6 +4,8 @@ #include +using namespace musica; + TEST(Musica, Version) { char* versions = getAllComponentVersions(); diff --git a/src/test/unit/micm/CMakeLists.txt b/src/test/unit/micm/CMakeLists.txt new file mode 100644 index 00000000..2049f1c3 --- /dev/null +++ b/src/test/unit/micm/CMakeLists.txt @@ -0,0 +1,4 @@ +include(test_util) + +create_standard_test_cxx(NAME connect_to_micm SOURCES micm.cpp) +create_standard_test_cxx(NAME micm_c_api SOURCES micm_c_api.cpp) \ No newline at end of file diff --git a/src/test/connections/micm.cpp b/src/test/unit/micm/micm.cpp similarity index 100% rename from src/test/connections/micm.cpp rename to src/test/unit/micm/micm.cpp diff --git a/src/test/connections/micm_c_api.cpp b/src/test/unit/micm/micm_c_api.cpp similarity index 99% rename from src/test/connections/micm_c_api.cpp rename to src/test/unit/micm/micm_c_api.cpp index de900a50..03137c84 100644 --- a/src/test/connections/micm_c_api.cpp +++ b/src/test/unit/micm/micm_c_api.cpp @@ -3,6 +3,8 @@ #include #include +using namespace musica; + // Test fixture for the MICM C API class MicmCApiTest : public ::testing::Test { protected: diff --git a/src/test/unit/tuvx/CMakeLists.txt b/src/test/unit/tuvx/CMakeLists.txt new file mode 100644 index 00000000..8960f011 --- /dev/null +++ b/src/test/unit/tuvx/CMakeLists.txt @@ -0,0 +1,12 @@ +include(test_util) + +create_standard_test_cxx(NAME tuvx_c_api SOURCES tuvx_c_api.cpp) + +################################################################################ +# Copy tuvx test data + +add_custom_target(copy_tuvx_test_configs ALL ${CMAKE_COMMAND} -E copy_directory + ${tuvx_SOURCE_DIR}/examples ${CMAKE_BINARY_DIR}/configs/tuvx) + +add_custom_target(copy_tuvx_data ALL ${CMAKE_COMMAND} -E copy_directory + ${tuvx_SOURCE_DIR}/data ${CMAKE_BINARY_DIR}/configs/tuvx/data) \ No newline at end of file diff --git a/src/test/unit/tuvx/tuvx_c_api.cpp b/src/test/unit/tuvx/tuvx_c_api.cpp new file mode 100644 index 00000000..b7d0dcaa --- /dev/null +++ b/src/test/unit/tuvx/tuvx_c_api.cpp @@ -0,0 +1,52 @@ +#include +#include + +using namespace musica; + +// Test fixture for the TUVX C API +class TuvxCApiTest : public ::testing::Test { +protected: + TUVX* tuvx; + int error_code; + const char* config_path; + + void SetUp(const char* configPath) { + tuvx = nullptr; + error_code = 0; + config_path = configPath; // Set the config path based on the parameter + tuvx = create_tuvx(config_path, &error_code); + } + + void TearDown() override { + delete_tuvx(tuvx); + } +}; + + +TEST_F(TuvxCApiTest, CreateTuvxInstanceWithYamlConfig) { + const char* yaml_config_path = "configs/tuvx/ts1_tsmlt.yml"; + SetUp(yaml_config_path); + ASSERT_EQ(error_code, 0); + ASSERT_NE(tuvx, nullptr); +} + +TEST_F(TuvxCApiTest, CreateTuvxInstanceWithJsonConfig) { + const char* json_config_path = "configs/tuvx/ts1_tsmlt.json"; + SetUp(json_config_path); + ASSERT_EQ(error_code, 0); + ASSERT_NE(tuvx, nullptr); +} + +TEST_F(TuvxCApiTest, DetectsNonexistentYamlConfigFile) { + const char* yaml_config_path = "nonexisting.yml"; + SetUp(yaml_config_path); + ASSERT_EQ(error_code, 2); + ASSERT_EQ(tuvx, nullptr); +} + +TEST_F(TuvxCApiTest, DetectsNonexistentJSONConfigFile) { + const char* json_config_path = "nonexisting.json"; + SetUp(json_config_path); + ASSERT_EQ(error_code, 2); + ASSERT_EQ(tuvx, nullptr); +} diff --git a/src/tuvx/CMakeLists.txt b/src/tuvx/CMakeLists.txt new file mode 100644 index 00000000..8f16a57e --- /dev/null +++ b/src/tuvx/CMakeLists.txt @@ -0,0 +1,5 @@ +target_sources(musica + PRIVATE + interface.F90 + tuvx.cpp +) \ No newline at end of file diff --git a/src/tuvx/interface.F90 b/src/tuvx/interface.F90 new file mode 100644 index 00000000..35a949e9 --- /dev/null +++ b/src/tuvx/interface.F90 @@ -0,0 +1,43 @@ +module tuvx_interface + + use iso_c_binding, only : c_ptr, c_loc, c_int + use tuvx_core, only : core_t + use musica_util, only : to_f_string, string_t_c + use musica_string, only : string_t + implicit none + + private + + contains + + function internal_create_tuvx(config_path, error_code) bind(C, name="internal_create_tuvx") + type(string_t_c), value, intent(in) :: config_path + integer(kind=c_int), intent(out) :: error_code + + type(c_ptr) :: internal_create_tuvx + type(core_t), pointer :: core + character(len=:), allocatable :: f_string + type(string_t) :: musica_config_path + + f_string = to_f_string(config_path) + musica_config_path = string_t(f_string) + + core => core_t(musica_config_path) + internal_create_tuvx = c_loc(core) + error_code = 0 + + end function internal_create_tuvx + + + subroutine internal_delete_tuvx(tuvx) bind(C, name="internal_delete_tuvx") + use iso_c_binding, only: c_ptr, c_f_pointer + type(c_ptr), intent(in) :: tuvx + type(core_t), pointer :: core + + call c_f_pointer(tuvx, core) + if (associated(core)) then + deallocate(core) + end if + end subroutine internal_delete_tuvx + +end module tuvx_interface diff --git a/src/tuvx/tuvx.cpp b/src/tuvx/tuvx.cpp new file mode 100644 index 00000000..8def0a59 --- /dev/null +++ b/src/tuvx/tuvx.cpp @@ -0,0 +1,54 @@ +/** + * This file contains the implementation of the TUVX class, which represents a multi-component + * reactive transport model. It also includes functions for creating and deleting TUVX instances, + * Copyright (C) 2023-2024 National Center for Atmospheric Research, + * + * SPDX-License-Identifier: Apache-2.0* creating solvers, and solving the model. + */ + +#include + +#include + +namespace musica { + +TUVX *create_tuvx(const char *config_path, int *error_code) +{ + try + { + TUVX *tuvx = new TUVX(); + *error_code = tuvx->create(std::string(config_path)); + return tuvx; + } + catch (const std::bad_alloc &e) + { + *error_code = 1; + return nullptr; + } + catch (const std::exception &e) + { + std::cout << e.what() << std::endl; + *error_code = 2; + return nullptr; + } +} + +void delete_tuvx(const TUVX *tuvx) +{ + delete tuvx; +} + +TUVX::~TUVX() +{ + int error_code = 0; + internal_delete_tuvx(tuvx_.get(), &error_code); +} + +int TUVX::create(const std::string &config_path) +{ + int parsing_status = 0; // 0 on success, 1 on failure + String config_path_str = ToString(const_cast(config_path.c_str())); + tuvx_ = std::make_unique(internal_create_tuvx(config_path_str, &parsing_status)); + return parsing_status; +} +} \ No newline at end of file diff --git a/fortran/util.F90 b/src/util.F90 similarity index 100% rename from fortran/util.F90 rename to src/util.F90 diff --git a/src/util.cpp b/src/util.cpp index 6483c618..7b2f5ae9 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -4,6 +4,8 @@ #include #include +namespace musica { + String ToString(char* value) { String str; @@ -25,16 +27,6 @@ void DeleteString(String str) delete[] str.value_; } -Error NoError() -{ - return ToError("", 0, "Success"); -} - -Error ToError(const char* category, int code) -{ - return ToError(category, code, ""); -} - Error ToError(const char* category, int code, const char* message) { Error error; @@ -44,11 +36,22 @@ Error ToError(const char* category, int code, const char* message) return error; } + +Error ToError(const char* category, int code) +{ + return ToError(category, code, ""); +} + Error ToError(const std::system_error& e) { return ToError(e.code().category().name(), e.code().value(), e.what()); } +Error NoError() +{ + return ToError("", 0, "Success"); +} + bool operator==(const Error& lhs, const Error& rhs) { if (lhs.code_ == 0 && rhs.code_ == 0) @@ -62,4 +65,6 @@ bool operator==(const Error& lhs, const Error& rhs) bool operator!=(const Error& lhs, const Error& rhs) { return !(lhs == rhs); +} + } \ No newline at end of file