diff --git a/.github/workflows/github-actions-on-github-servers.yml b/.github/workflows/github-actions-on-github-servers.yml new file mode 100644 index 0000000..51747c9 --- /dev/null +++ b/.github/workflows/github-actions-on-github-servers.yml @@ -0,0 +1,102 @@ +name: actions_on_github_servers + +on: + # trigger the workflow upon request + workflow_dispatch: + # trigger the workflow on push or pull request, + # but only for the main branch + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +jobs: + run_tests: + # we use jobs in a matrix + # the OS to be used for the test is taken from the current matrix element + runs-on: ${{ matrix.os }} + strategy: + # we want to avoid to stop all the tests the first time that one of them gets an error + fail-fast: false + matrix: + include: + #here we set up the various matrix elements + #the entries in each matrix element are just variables, not keywords, with (hopefully) self-explaining names + # 1st matrix element, current LTS Ubuntu distribution, default bash version + - os: ubuntu-22.04 + bash_version: "default" + # 2st matrix element, current LTS Ubuntu distribution, bash 4.4 + - os: ubuntu-22.04 + bash_version: "bash-4.4" + steps: + # this is an action provided by GitHub to checkout the repository + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + cache: false + # taken from https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + - name: Set up Python 3.x + uses: actions/setup-python@v4 + with: + # pick the latest minor release of Python 3 + python-version: '3.x' + # print the picked Python version + - name: Display Python version + run: python -c "import sys; print(sys.version)" + # this Action should follow steps to set up Python build environment + - name: Install codebase Python dependencies + uses: py-actions/py-dependency-install@v4 + with: + path: "python/requirements.txt" + - name: Install further Python dependencies for tests + uses: py-actions/py-dependency-install@v4 + with: + path: "tests/python_requirements.txt" + # now everything should be ready to setup bash and run codebase tests + - name: code_check + # we set some environment variables which depend on the specific matrix element + env: + OS_NAME: ${{ matrix.os }} + BASH_TESTED_VERSION: ${{matrix.bash_version}} + # we run the step + # we recall that in YAML the pipe symbol "|" means that the following lines, including newlines, are interpreted literally + run: | + # we save in a variable the path of the starting directory + export main_dir="${PWD}" + # we install the formatter + export PATH="${PATH}:$(go env GOPATH)/bin" + go install mvdan.cc/sh/v3/cmd/shfmt@latest + printf "\n$(pwd)\nInstalled shfmt at $(which shfmt): $(shfmt --version)\n\n" + # if we are not using the default bash version, we download and compile the desired one + if [ ${BASH_TESTED_VERSION} != "default" ]; then + cd ${HOME} + printf "Download and compile bash-${BASH_TESTED_VERSION} into ${HOME}...\n" + wget -nv https://ftp.gnu.org/gnu/bash/${BASH_TESTED_VERSION}.tar.gz + tar xf ${BASH_TESTED_VERSION}.tar.gz + output_file=configure_make.log + cd ${BASH_TESTED_VERSION} + ./configure > "${output_file}" 2>&1 || { cat "${output_file}"; exit 1; } + make -j$(nproc) > "${output_file}" 2>&1 || { cat "${output_file}"; exit 1; } + BASH_EXE=$(pwd)/bash + # now we install yq in the minimum version supported by the handler (otherwise a more recent version is available in GitHub actions) + printf "Download yq-v4.24.2...\n" + wget -nv -O /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.24.2/yq_linux_amd64 + chmod +x /usr/local/bin/yq + else + BASH_EXE=$(which bash) + fi + # we print the bash version to check that we used the desired one + printf "\n$(${BASH_EXE} --version)\n" + # we set the TERM environment variable (used by utilities exploited in the tests) + export TERM="xterm" + # now we enter into the tests directory and execute the tests with maximum detail (-r 3) + cd ${main_dir}/tests + ${BASH_EXE} tests_runner unit -r 3 -k ||\ + { printf "\nDetailed output of unit tests for debugging purposes:\n"; cat run_tests/tests_runner.log; exit 1; } + rm -rf run_tests + ${BASH_EXE} tests_runner functional -r 3 -k ||\ + { printf "\nDetailed output of functional tests for debugging purposes:\n"; cat run_tests/tests_runner.log; exit 1; } diff --git a/.gitignore b/.gitignore index df076c6..6237d7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -build* +build*/ binaries -.DS_Store python_scripts/*.pyc + +# OS specific auxiliary files +.vscode +.DS_Store diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..0a2e8cc --- /dev/null +++ b/.mailmap @@ -0,0 +1,18 @@ +Alessandro Sciarra +Alessandro Sciarra +Anna Schäfer +Anna Schäfer <36138176+akschaefer@users.noreply.github.com> +Gabriele Inghirami <71900834+gabriele-inghirami@users.noreply.github.com> +Gabriele Inghirami gabriele-inghirami +Niklas Götz +Niklas Götz +Niklas Götz +Niklas Götz Niklas Goetz +Nils Saß +Nils Saß <90659054+nilssass@users.noreply.github.com> +Jan Hammelmann +Jan Hammelmann <33685583+JanHammelmann@users.noreply.github.com> +Renan Hirayama +Renan Hirayama RenHirayama <79031685+RenHirayama@users.noreply.github.com> +Zuzana Paulinyova +Zuzana Paulinyova zpauliny <103072930+zpauliny@users.noreply.github.com> diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..fa944a0 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,19 @@ +## Present developers and maintainers + +| Name | E-mail | +| :---: | :----: | +| Alessandro Sciarra | asciarra@fias.uni-frankfurt.de | +| Gabriele Inghirami | g.inghirami@gsi.de | +| Jan Hammelmann | hammelmann@fias.uni-frankfurt.de | +| Niklas Götz | goetz@fias.uni-frankfurt.de | +| Nils Saß | nsass@fias.uni-frankfurt.de | +| Renan Hirayama | hirayama@fias.uni-frankfurt.de | +| Zuzana Paulinyova | zuzana.paulinyova@upjs.sk | + +## Past developers + +Please, note that the e-mail address of past developers might not be active any more. + +| Name | E-mail | +| :----: | :----: | +| Anna Schäfer | aschaefer@fias.uni-frankfurt.de | diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 1b9cc0e..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,1168 +0,0 @@ -# The project name -project(annas-hybrid NONE C) - -cmake_minimum_required(VERSION 3.5.1) - -## Tell cmake where to find the modules -set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") -include(FindPythonModules) - -find_package(PythonInterp 3.6 REQUIRED) -find_package(PythonLibs 3.6) - -find_python_module(numpy VERSION 1.8.2 REQUIRED) -find_python_module(matplotlib VERSION 1.3.1 REQUIRED) -find_python_module(argparse VERSION 1.1 REQUIRED) - -# Find and copy SMASH -find_program(SMASH smash PATHS ${SMASH_PATH} NO_DEFAULT_PATH) -if(NOT SMASH) - message(FATAL_ERROR - "SMASH not found. Please specify a path to the SMASH build directory " - "by passing '-DSMASH_PATH=...' to cmake.") -else() - message(STATUS "Found SMASH: ${SMASH}") - # copy executable - file(COPY ${SMASH} DESTINATION ${CMAKE_BINARY_DIR}) -endif() - -# Find and copy vHLLE -find_program(vHLLE hlle_visc PATHS ${VHLLE_PATH} NO_DEFAULT_PATH) -if(NOT vHLLE) - message(FATAL_ERROR - "vHLLE not found. Please specify a path to the vHLLE directory " - "by passing '-DVHLLE_PATH=...' to cmake.") -else() - message(STATUS "Found vHLLE: ${vHLLE}") - # copy executable - file(COPY ${vHLLE} DESTINATION ${CMAKE_BINARY_DIR}) -endif() - -# find parameters: equations of state -find_path(vHLLE_EoSs eos PATHS ${VHLLE_PARAMS_PATH} NO_DEFAULT_PATH) -if(NOT vHLLE_EoSs) - message(FATAL_ERROR - "Equations of state not found. Please specify a path to the vHLLE params " - "directory by passing '-DVHLLE_PARAMS_PATH=...' to cmake.") -else() - message(STATUS "Found EoSs: ${VHLLE_PARAMS_PATH}/eos") - # copy EoS files - file(COPY "${VHLLE_PARAMS_PATH}/eos" DESTINATION ${CMAKE_BINARY_DIR}) -endif() - -# Find and copy sampler -find_program(SAMPLER sampler PATHS ${SAMPLER_PATH} NO_DEFAULT_PATH) -if(NOT SAMPLER) - message(FATAL_ERROR - "Sampler not found. Please specify a path to the Sampler build directory " - "by passing '-DSAMPLER_PATH=...' to cmake.") -else() - message(STATUS "Found Sampler: ${SAMPLER}") - # copy executable - file(COPY ${SAMPLER} DESTINATION ${CMAKE_BINARY_DIR}) -endif() - -# Decide whether to automatically analyze the data by means of the -# SMASH-analysis. If so, the path to the analysis suite needs to be passed. -if(SMASH_ANALYSIS_PATH) - option(WITH_ANALYSIS "Automatically analyze output to yield y, mT, pT, v2, N, ... tables." ON) -endif(SMASH_ANALYSIS_PATH) - -if(WITH_ANALYSIS) - if(SMASH_ANALYSIS_PATH) - message(STATUS "Found SMASH-analysis: ${SMASH_ANALYSIS_PATH}") - message(STATUS "Automatic analysis of results is available.") - else() - message(WARNING "SMASH-analysis not found. Provide path to SMASH-analysis directory via - cmake .. -DSMASH_ANALYSIS_PATH=path/to/SMASH_ANALYSIS") - endif(SMASH_ANALYSIS_PATH) -endif(WITH_ANALYSIS) - - -# Decide whether to automatically analyze the data regarding flow (v2, v3). -# If so, the path to the archive needs to be passed. -if(ARCHIVE_PATH) - option(WITH_FLOW "Automatically analyze output for v2, v3." ON) -endif(ARCHIVE_PATH) - -if(WITH_FLOW) - if(ARCHIVE_PATH) - message(STATUS "Found archive: ${ARCHIVE_PATH}") - message(STATUS "Automatic flow analysis is available.") - else() - message(WARNING "archive. Provide path to the archive directory via - cmake .. -DARCHIVE_PATH=path/to/archive") - endif(ARCHIVE_PATH) -endif(WITH_FLOW) - - -#----------------------------------------------------------------------------# -# Function to set up hybrid evolution depending on system and energy -#----------------------------------------------------------------------------# - -function(run_one_energy - energy_and_system - ) - list(GET energy_and_system 0 energy) - list(GET energy_and_system 1 system) - message(STATUS "Configuring hybrid run for ${system} @ sqrt(s) = ${energy} GeV.") - set(results_folder "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}") - file(MAKE_DIRECTORY ${results_folder}) - - set(smash_IC_files "") - set(vhlle_freezeout_hypersurfaces "") - set(sampled_particle_lists "") - set(final_particle_lists "") - set(all_analysis_outputs "") - set(all_flow_outputs "") - set(all_plots "") - set(all_conservation_plots "") - - set(num_runs "100") - foreach(i RANGE 1 ${num_runs}) - file(MAKE_DIRECTORY "${results_folder}/${i}/IC") # subdir for initial conditions - file(MAKE_DIRECTORY "${results_folder}/${i}/Hydro") # subdir for hydro run - file(MAKE_DIRECTORY "${results_folder}/${i}/Sampler") # subdir for particle sampling - file(MAKE_DIRECTORY "${results_folder}/${i}/Afterburner") # subdir for afterburner - file(MAKE_DIRECTORY "${results_folder}/${i}/Spectra") # subdir for analyzed spectra and plots - - # Set variables for files - set(smash_IC_file "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/IC/SMASH_IC.dat") - set(smash_IC_oscar "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/IC/SMASH_IC.oscar") - set(smash_IC_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/smash_initial_conditions_${system}.yaml") - set(vhlle_default_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/vhlle_hydro") - set(vhlle_output_dir "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/Hydro/") - set(vhlle_updated_config "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/Hydro/vhlle_config") - set(vhlle_freezeout_hypersurface "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/Hydro/freezeout.dat") - set(sampler_input_hypersurface "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/Sampler/freezeout.dat") - set(sampler_default_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/hadron_sampler") - set(sampler_updated_config "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/Sampler/sampler_config") - set(sampler_dir "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/Sampler") - set(sampled_particle_list "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/Sampler/particle_lists.oscar") - set(full_particle_list "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/Sampler/sampling0") - set(smash_afterburner_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/smash_afterburner.yaml") - set(final_particle_list "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/Afterburner/particles_binary.bin") - - list(APPEND smash_IC_files ${smash_IC_file}) - list(APPEND vhlle_freezeout_hypersurfaces ${vhlle_freezeout_hypersurface}) - list(APPEND sampled_particle_lists ${sampled_particle_list}) - list(APPEND final_particle_lists ${final_particle_list}) - - #----------------------------------------------------------------------------# - # Run SMASH and generate initial conditions output - #----------------------------------------------------------------------------# - add_custom_command(OUTPUT "${smash_IC_file}" "${results_folder}/${i}/config.yaml" - COMMAND "${CMAKE_BINARY_DIR}/smash" - "-o" "${results_folder}/${i}/IC" - "-i" "${smash_IC_config}" - "-c" "Modi: {Collider: {Sqrtsnn: ${energy}}}" - "-f" - "-n" - ">" "${results_folder}/${i}/IC/Terminal_Output.txt" - DEPENDS - "${smash_IC_config}" - "${CMAKE_BINARY_DIR}/smash" - COMMENT "Running SMASH for Initial Conditions of ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - #----------------------------------------------------------------------------# - # Feed SMASH initial conditions into vHLLE and run hydro evolution - #----------------------------------------------------------------------------# - # create input file with correct paths for vHLLE - add_custom_command(OUTPUT "${vhlle_updated_config}" - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_vhlle_config.py" - "--vhlle_config" "${vhlle_default_config}" - "--smash_ic" "${smash_IC_file}" - "--output_file" "${vhlle_updated_config}" - "--energy" "${energy}" - DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_vhlle_config.py" - "${vhlle_default_config}" - COMMENT "Creating input file for vHLLE for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - - # Run hydro - add_custom_command(OUTPUT "${vhlle_freezeout_hypersurface}" - COMMAND "${CMAKE_BINARY_DIR}/hlle_visc" - "-params" "${vhlle_updated_config}" - "-ISinput" "${smash_IC_file}" - "-outputDir" "${vhlle_output_dir}" - ">" "${vhlle_output_dir}/Terminal_Output.txt" - DEPENDS - "${vhlle_updated_config}" - "${CMAKE_BINARY_DIR}/hlle_visc" - COMMENT "Running vHLLE for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - #----------------------------------------------------------------------------# - # Run Cooper-Frye sampler for particlization of freezeout surface - #----------------------------------------------------------------------------# - # create input file with correct paths for sampler - set(N_events_afterburner "1000") - add_custom_command(OUTPUT "${sampler_updated_config}" - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_sampler_config.py" - "--sampler_config" "${sampler_default_config}" - "--vhlle_config" "${vhlle_updated_config}" - "--output_file" "${sampler_updated_config}" - "--Nevents" "${N_events_afterburner}" - DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_sampler_config.py" - "${sampler_default_config}" - COMMENT "Creating input file for sampler for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - # Run sampler - add_custom_command(OUTPUT ${sampled_particle_list} - COMMAND "${CMAKE_BINARY_DIR}/sampler" "events" "1" "${sampler_updated_config}" - ">" "${results_folder}/${i}/Sampler/Terminal_Output.txt" - DEPENDS - "${CMAKE_BINARY_DIR}/sampler" - "${sampler_updated_config}" - COMMENT "Sampling particles from freezeout hypersurface (${i}/${num_runs})." - ) - - #----------------------------------------------------------------------------# - # Run SMASH as afterburner - #----------------------------------------------------------------------------# - # Add spectators to particle list and rename it to be in accordance - # with SMASH list modus input format - add_custom_command(OUTPUT ${full_particle_list} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/add_spectators.py" - "--sampled_particle_list" "${sampler_dir}/particle_lists.oscar" - "--initial_particle_list" "${smash_IC_oscar}" - "--output_file" "${full_particle_list}" - "--smash_config" "${results_folder}/${i}/IC/config.yaml" - DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/add_spectators.py" - COMMENT "Adding spectators to sampled particle list for afterburner (${i}/${num_runs})." - ) - - # Run afterburner evolution - set(setMonash FALSE) - if (energy GREATER_EQUAL 200) - set(setMonash TRUE) - endif() - - add_custom_command(OUTPUT "${final_particle_list}" "${results_folder}/${i}/Afterburner/config.yaml" - COMMAND "${CMAKE_BINARY_DIR}/smash" - "-i" "${smash_afterburner_config}" - "-c" "Modi: { List: { File_Directory: ${sampler_dir}} }" - "-c" "General: { Nevents: ${N_events_afterburner} }" - "-c" "Collision_Term: { String_Parameters: {Use_Monash_Tune: ${setMonash} } }" - "-o" "${results_folder}/${i}/Afterburner" - "-f" - "-n" - ">" "/dev/null" - DEPENDS - "${smash_afterburner_config}" - "${CMAKE_BINARY_DIR}/smash" - "${full_particle_list}" - COMMENT "Running SMASH afterburner for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - #----------------------------------------------------------------------------# - # Analyse data - #----------------------------------------------------------------------------# - # Define filenames for particle spectra and create list of those - set(spectra_fnames "${results_folder}/${i}/Spectra/yspectra.txt" ; - "${results_folder}/${i}/Spectra/mtspectra.txt" ; - "${results_folder}/${i}/Spectra/ptspectra.txt" ; - "${results_folder}/${i}/Spectra/v2spectra.txt" ; - "${results_folder}/${i}/Spectra/meanmt0_midrapidity.txt" ; - "${results_folder}/${i}/Spectra/meanpt_midrapidity.txt" ; - "${results_folder}/${i}/Spectra/midrapidity_yield.txt" ; - "${results_folder}/${i}/Spectra/total_multiplicity.txt") - set(analysis_outputs "") - foreach(j ${spectra_fnames}) - list(APPEND analysis_outputs "${j}") - endforeach(j) - - list(APPEND all_analysis_outputs ${analysis_outputs}) - - # Perform analysis - add_custom_command(OUTPUT ${analysis_outputs} - COMMAND "python3" "${SMASH_ANALYSIS_PATH}/test/energy_scan/mult_and_spectra.py" - "--output_files" ${analysis_outputs} - "--input_files" "${final_particle_list}" - COMMENT "Analyzing spectra for ${system} @ ${energy} GeV (${i}/${num_runs})." - DEPENDS "${SMASH_ANALYSIS_PATH}/test/energy_scan/mult_and_spectra.py" - ) - - #----------------------------------------------------------------------------# - # Plot spectra - #----------------------------------------------------------------------------# - # Define plot names - set(plot_names "${results_folder}/${i}/Spectra/yspectra.pdf" ; - "${results_folder}/${i}/Spectra/mtspectra.pdf" ; - "${results_folder}/${i}/Spectra/ptspectra.pdf" ; - "${results_folder}/${i}/Spectra/v2spectra.pdf" ; - "${results_folder}/${i}/Spectra/meanmt0_midrapidity.pdf" ; - "${results_folder}/${i}/Spectra/meanpt_midrapidity.pdf" ; - "${results_folder}/${i}/Spectra/midrapidity_yield.pdf" ; - "${results_folder}/${i}/Spectra/total_multiplicity.pdf") - - set(plots "") - foreach(j ${plot_names}) - list(APPEND plots "${j}") - endforeach(j) - - list(APPEND all_plots ${plots}) - - # Perform plotting - add_custom_command(OUTPUT ${plots} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/plot_spectra.py" - "--input_files" "${results_folder}/${i}/Spectra/*.txt" - "--energy" "${energy}" - "--system" "${system}" - "--Nevents" "${N_events_afterburner}" - COMMENT "Plotting spectra for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - - - #----------------------------------------------------------------------------# - # Check conserved quantities - #----------------------------------------------------------------------------# - set(smash_IC_binary "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/IC/SMASH_IC.bin") - set(output_dir "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/${system}_${energy}/${i}/") - set(E_cons_plot "${output_dir}/Energy_Conservation.pdf") - set(Mult_plot "${output_dir}/Pions.pdf") - set(Hydro_lifetime_list "${output_dir}/Hydro_lifetime.txt") - - list(APPEND all_conservation_plots ${E_cons_plot}) - list(APPEND all_multiplicity_plots ${Mult_plot}) - list(APPEND all_lifetime_lists ${Hydro_lifetime_list}) - add_custom_command(OUTPUT ${E_cons_plot} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/check_conservation.py" - "--SMASH_IC" "${smash_IC_oscar}" - "--Hydro_Info" "${results_folder}/${i}/Hydro/Terminal_Output.txt" - "--Sampler" "${full_particle_list}" - "--SMASH_final_state" "${final_particle_list}" - "--SMASH_ana_path" "${SMASH_ANALYSIS_PATH}" - "--output_path" "${output_dir}" - "--Nevents" "${N_events_afterburner}" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/check_conservation.py" - COMMENT "Checking conservation laws for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - add_custom_command(OUTPUT ${Mult_plot} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/check_multiplicities.py" - "--Freezeout_Surface" "${vhlle_freezeout_hypersurface}" - "--Sampler" "${sampled_particle_list}" - "--output_path" "${output_dir}" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/check_multiplicities.py" - COMMENT "Verifying multiplicities for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - add_custom_command(OUTPUT ${Hydro_lifetime_list} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/get_hydro_endtime.py" - "--Hydro_Info" "${results_folder}/${i}/Hydro/Terminal_Output.txt" - "--output_path" "${output_dir}" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/get_hydro_endtime.py" - COMMENT "Find hydro lifetime in ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - #----------------------------------------------------------------------------# - # Analyze flow - #----------------------------------------------------------------------------# - # Define filenames for particle v2 and v3 output - set(flow_fnames "${results_folder}/${i}/Spectra/EP_v2.txt" ; - "${results_folder}/${i}/Spectra/EP_v3.txt") - set(flow_outputs "") - foreach(j ${flow_fnames}) - list(APPEND flow_outputs "${j}") - endforeach(j) - - list(APPEND all_flow_outputs ${flow_outputs}) - - # Perform flow analysis (with STAR cuts) - add_custom_command(OUTPUT "${results_folder}/${i}/Spectra/EP_v2.txt" - COMMAND "python3" "${ARCHIVE_PATH}/flow/python_scripts/flow_calculation.py" - "particle_lists.oscar" "${results_folder}/${i}/Afterburner" "${results_folder}/${i}/Spectra/" - "EP_v2.txt" "2" "pT" "event_plane" "${energy}" - "211,-211,321,-321,2212,-2212,3222,-3222,3112,-3112,3312,-3312,3334,-3334" - "eta" "-0.5" "0.5" "false" "0.0" "4.0" "20" - "false" - COMMENT "Analyzing v2 for ${system} @ ${energy} GeV (${i}/${num_runs})." - DEPENDS "${ARCHIVE_PATH}/flow/python_scripts/flow_calculation.py" - - ) - - add_custom_command(OUTPUT "${results_folder}/${i}/Spectra/EP_v3.txt" - COMMAND "python3" "${ARCHIVE_PATH}/flow/python_scripts/flow_calculation.py" - "particle_lists.oscar" "${results_folder}/${i}/Afterburner" "${results_folder}/${i}/Spectra/" - "EP_v3.txt" "3" "pT" "event_plane" "${energy}" - "211,-211,321,-321,2212,-2212,3222,-3222,3112,-3112,3312,-3312,3334,-3334" - "eta" "-0.5" "0.5" "false" "0.0" "4.0" "20" - "false" - COMMENT "Analyzing v3 for ${system} @ ${energy} GeV (${i}/${num_runs})." - DEPENDS "${ARCHIVE_PATH}/flow/python_scripts/flow_calculation.py" - - ) - - endforeach(i) - - #----------------------------------------------------------------------------# - # Average previously obtained spectra - #----------------------------------------------------------------------------# - file(MAKE_DIRECTORY "${results_folder}/Averaged_Spectra") - set(averaged_spectra "${results_folder}/pT.txt" ; - "${results_folder}/mT.txt" ; - "${results_folder}/dNdy.txt" - ) - add_custom_command(OUTPUT ${averaged_spectra} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/average_spectra.py" - "--smash_ana_dir" "${SMASH_ANALYSIS_PATH}" - "--pT_files" "${results_folder}/*/Spectra/ptspectra.txt" - "--mT_files" "${results_folder}/*/Spectra/mtspectra.txt" - "--y_files" "${results_folder}/*/Spectra/yspectra.txt" - "--midyY_files" "${results_folder}/*/Spectra/midrapidity_yield.txt" - "--meanpT_files" "${results_folder}/*/Spectra/meanpt_midrapidity.txt" - "--v2_files" "${results_folder}/*/Spectra/v2spectra.txt" - "--Nevents" "${N_events_afterburner}" - "--output_dir" "${results_folder}/Averaged_Spectra/" - COMMENT "Averaging spectra for ${system} @ ${energy} GeV." - ) - - #----------------------------------------------------------------------------# - # Average previously obtained flow - #----------------------------------------------------------------------------# - set(averaged_flow_spectra "${results_folder}/v2.txt" ; - "${results_folder}/v3.txt" ; - ) - add_custom_command(OUTPUT ${averaged_flow_spectra} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/average_flow.py" - "--v2_files" "${results_folder}/*/Spectra/EP_v2.txt" - "--v3_files" "${results_folder}/*/Spectra/EP_v3.txt" - "--energy" "${energy}" - "--output_dir" "${results_folder}/Averaged_Spectra/" - COMMENT "Averaging flow for ${system} @ ${energy} GeV." - ) - - #----------------------------------------------------------------------------# - # Average previously obtained endtimes - #----------------------------------------------------------------------------# - set(averaged_endtime_files "${results_folder}/Endtime.txt") - add_custom_command(OUTPUT ${averaged_endtime_files} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/average_endtimes.py" - "--endtime_files" "${results_folder}/*/Hydro_Endtime.txt" - "--energy" "${energy}" - "--output_dir" "${results_folder}/Averaged_Spectra/" - COMMENT "Averaging flow for ${system} @ ${energy} GeV." - ) - - #----------------------------------------------------------------------------# - # Plot averaged quantities - #----------------------------------------------------------------------------# - set(averaged_plots "${results_folder}/Averaged_Spectra/yspectra.pdf" ; - "${results_folder}/Averaged_Spectra/mtspectra.pdf" ; - "${results_folder}/Averaged_Spectra/ptspectra.pdf") - - # Perform plotting of averaged quantities - add_custom_command(OUTPUT ${averaged_plots} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/plot_spectra.py" - "--input_files" "${results_folder}/Averaged_Spectra/*.txt" - "--energy" "${energy}" - "--system" "${system}" - "--Nevents" "${N_events_afterburner}" - "--average" "True" - COMMENT "Plotting averaged spectra for ${system} @ ${energy} GeV." - ) - - - # Define subtargets to enable separated running of the hybrid submodules - - set(target ${system}_${energy}_IC) - add_custom_target(${target} ALL DEPENDS ${smash_IC_files}) - - set(target ${system}_${energy}_hydro) - add_custom_target(${target} ALL DEPENDS ${vhlle_freezeout_hypersurfaces}) - - set(target ${system}_${energy}_sampler) - add_custom_target(${target} ALL DEPENDS ${sampled_particle_lists}) - - set(target ${system}_${energy}_afterburner) - add_custom_target(${target} ALL DEPENDS ${final_particle_lists}) - - set(target ${system}_${energy}_analysis) - add_custom_target(${target} ALL DEPENDS ${all_analysis_outputs}) - - set(target ${system}_${energy}_flow) - add_custom_target(${target} ALL DEPENDS ${all_flow_outputs}) - - set(target ${system}_${energy}_plots) - add_custom_target(${target} ALL DEPENDS ${all_plots}) - - set(target ${system}_${energy}_check_conservation) - add_custom_target(${target} ALL DEPENDS ${all_conservation_plots}) - - set(target ${system}_${energy}_check_multiplicities) - add_custom_target(${target} ALL DEPENDS ${all_multiplicity_plots}) - - set(target ${system}_${energy}_hydro_endtime) - add_custom_target(${target} ALL DEPENDS ${all_lifetime_lists}) - - # Averaging - set(target ${system}_${energy}_average_spectra) - add_custom_target(${target} ALL DEPENDS ${averaged_spectra}) - - set(target ${system}_${energy}_average_flow) - add_custom_target(${target} ALL DEPENDS ${averaged_flow_spectra}) - - set(target ${system}_${energy}_average_plots) - add_custom_target(${target} ALL DEPENDS ${averaged_plots}) - - set(target ${system}_${energy}_average_endtime) - add_custom_target(${target} ALL DEPENDS ${averaged_endtime_files}) - -endfunction() - -run_one_energy("4.3;AuAu") -run_one_energy("6.4;PbPb") -run_one_energy("7.7;AuAu") -run_one_energy("8.8;PbPb") -run_one_energy("17.3;PbPb") -run_one_energy("27.0;AuAu") -run_one_energy("39.0;AuAu") -run_one_energy("62.4;AuAu") -run_one_energy("130.0;AuAu") -run_one_energy("200.0;AuAu") -run_one_energy("2760.0;PbPb") -run_one_energy("5020.0;PbPb") - - -function(Excitation_Funcs - ) - set(outputdir "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results") - set(midY_plot "${outputdir}/midy_yield_exc_func.pdf") - add_custom_command(OUTPUT ${midY_plot} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/plot_excitation_functions.py" - "--midyY_files" "${outputdir}/*/Averaged_Spectra/midyY.txt" - "--v2_files" "${outputdir}/*/Averaged_Spectra/v2spectra.txt" - "--meanpT_files" "${outputdir}/*/Averaged_Spectra/meanpT.txt" - "--output_dir" "${outputdir}/" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/plot_excitation_functions.py" - COMMENT "Plotting excitation functions." - ) - set(target exc_funcs) - add_custom_target(${target} ALL DEPENDS ${midY_plot}) -endfunction() - - -function(Integrated_vn - ) - set(outputdir "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results") - set(int_vn_plot "${outputdir}/int_vn.pdf") - add_custom_command(OUTPUT ${int_vn_plot} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/plot_int_vn.py" - "--vn_files" "${outputdir}/*/Averaged_Spectra/int_vn.txt" - "--output_dir" "${outputdir}/" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/plot_int_vn.py" - COMMENT "Plotting integrated vn excitation functions." - ) - set(target int_vn) - add_custom_target(${target} ALL DEPENDS ${int_vn_plot}) -endfunction() - - -function(Hydro_Lifetimes - ) - set(outputdir "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results") - set(lifetime_file "${outputdir}/hydro_lifetime.txt") - add_custom_command(OUTPUT ${lifetime_file} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/assemble_endtimes.py" - "--endtime_files" "${outputdir}/*/Averaged_Spectra/Endtime.txt" - "--output_dir" "${outputdir}/" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/assemble_endtimes.py" - COMMENT "Plotting integrated vn excitation functions." - ) - set(target hydro_lifetimes) - add_custom_target(${target} ALL DEPENDS ${lifetime_file}) -endfunction() - - -Excitation_Funcs() -Integrated_vn() -Hydro_Lifetimes() - - -################################################################################ -# -# Add functionality to use any custom SMASH config for the initial conditions -# -################################################################################ -function(run_custom_hybrid - energy - ) - message(STATUS "Configuring hybrid run for custom IC config @ sqrt(s) = ${energy} GeV.") - set(results_folder "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/Custom") - file(MAKE_DIRECTORY ${results_folder}) - - set(smash_IC_files "") - set(vhlle_freezeout_hypersurfaces "") - set(sampled_particle_lists "") - set(final_particle_lists "") - set(all_analysis_outputs "") - set(all_plots "") - - set(num_runs "100") - foreach(i RANGE 1 ${num_runs}) - file(MAKE_DIRECTORY "${results_folder}/${i}/IC") # subdir for initial conditions - file(MAKE_DIRECTORY "${results_folder}/${i}/Hydro") # subdir for hydro run - file(MAKE_DIRECTORY "${results_folder}/${i}/Sampler") # subdir for particle sampling - file(MAKE_DIRECTORY "${results_folder}/${i}/Afterburner") # subdir for afterburner - file(MAKE_DIRECTORY "${results_folder}/${i}/Spectra") # subdir for analyzed spectra and plots - - set(smash_IC_file "${results_folder}/${i}/IC/SMASH_IC.dat") - set(smash_IC_oscar "${results_folder}/${i}/IC/SMASH_IC.oscar") - set(smash_IC_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/smash_initial_conditions_custom.yaml") - set(vhlle_default_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/vhlle_hydro") - set(vhlle_output_dir "${results_folder}/${i}/Hydro/") - set(vhlle_updated_config "${results_folder}/${i}/Hydro/vhlle_config") - set(vhlle_freezeout_hypersurface "${results_folder}/${i}/Hydro/freezeout.dat") - set(sampler_input_hypersurface "${results_folder}/${i}/Sampler/freezeout.dat") - set(sampler_default_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/hadron_sampler") - set(sampler_updated_config "${results_folder}/${i}/Sampler/sampler_config") - set(sampler_dir "${results_folder}/${i}/Sampler") - set(sampled_particle_list "${results_folder}/${i}/Sampler/particle_lists.oscar") - set(full_particle_list "${results_folder}/${i}/Sampler/sampling0") - set(smash_afterburner_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/smash_afterburner.yaml") - set(final_particle_list "${results_folder}/${i}/Afterburner/particles_binary.bin") - - list(APPEND smash_IC_files ${smash_IC_file}) - list(APPEND vhlle_freezeout_hypersurfaces ${vhlle_freezeout_hypersurface}) - list(APPEND sampled_particle_lists ${sampled_particle_list}) - list(APPEND final_particle_lists ${final_particle_list}) - - #----------------------------------------------------------------------------# - # Run SMASH and generate initial conditions output - #----------------------------------------------------------------------------# - add_custom_command(OUTPUT "${smash_IC_file}" "${results_folder}/${i}/IC/config.yaml" - COMMAND "${CMAKE_BINARY_DIR}/smash" - "-o" "${results_folder}/${i}/IC" - "-i" "${smash_IC_config}" - "-c" "Modi: {Collider: {Sqrtsnn: ${energy}}}" - "-f" - "-n" - ">" "${results_folder}/${i}/IC/Terminal_Output.txt" - DEPENDS - "${smash_IC_config}" - "${CMAKE_BINARY_DIR}/smash" - COMMENT "Running SMASH for initial conditions of custom hybrid run." - ) - - - #----------------------------------------------------------------------------# - # Feed SMASH initial conditions into vHLLE and run hydro evolution - #----------------------------------------------------------------------------# - # create input file with correct paths for vHLLE - add_custom_command(OUTPUT "${vhlle_updated_config}" - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_vhlle_config.py" - "--vhlle_config" "${vhlle_default_config}" - "--smash_ic" "${smash_IC_file}" - "--output_file" "${vhlle_updated_config}" - "--energy" "${energy}" - DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_vhlle_config.py" - "${vhlle_default_config}" - COMMENT "Creating input file for vHLLE for custom hybrid run." - ) - - - # Run hydro - add_custom_command(OUTPUT "${vhlle_freezeout_hypersurface}" - COMMAND "${CMAKE_BINARY_DIR}/hlle_visc" - "-params" "${vhlle_updated_config}" - "-ISinput" "${smash_IC_file}" - "-outputDir" "${vhlle_output_dir}" - ">" "${vhlle_output_dir}/Terminal_Output.txt" - DEPENDS - "${vhlle_updated_config}" - "${CMAKE_BINARY_DIR}/hlle_visc" - COMMENT "Running vHLLE for custom hybrid run." - ) - - #----------------------------------------------------------------------------# - # Run Cooper-Frye sampler for particlization of freezeout surface - #----------------------------------------------------------------------------# - # create input file with correct paths for sampler - set(N_events_afterburner "1000") - add_custom_command(OUTPUT "${sampler_updated_config}" - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_sampler_config.py" - "--sampler_config" "${sampler_default_config}" - "--vhlle_config" "${vhlle_updated_config}" - "--output_file" "${sampler_updated_config}" - "--Nevents" "${N_events_afterburner}" - DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_sampler_config.py" - "${sampler_default_config}" - COMMENT "Creating input file for custom hybrid run." - ) - - # Run sampler - add_custom_command(OUTPUT ${sampled_particle_list} - COMMAND "${CMAKE_BINARY_DIR}/sampler" "events" "1" "${sampler_updated_config}" - ">" "${results_folder}/${i}/Sampler/Terminal_Output.txt" - DEPENDS - "${CMAKE_BINARY_DIR}/sampler" - "${sampler_updated_config}" - COMMENT "Sampling particles from freezeout hypersurface (${i}/${num_runs})." - ) - - - #----------------------------------------------------------------------------# - # Run SMASH as afterburner - #----------------------------------------------------------------------------# - # Add spectators to particle list and rename it to be in accordance - # with SMASH list modus input format - add_custom_command(OUTPUT ${full_particle_list} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/add_spectators.py" - "--sampled_particle_list" "${sampler_dir}/particle_lists.oscar" - "--initial_particle_list" "${smash_IC_oscar}" - "--output_file" "${full_particle_list}" - "--smash_config" "${results_folder}/${i}/IC/config.yaml" - DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/add_spectators.py" - COMMENT "Adding spectators to sampled particle list for afterburner (${i}/${num_runs})." - ) - - # Run afterburner evolution - set(setMonash FALSE) - if (energy GREATER_EQUAL 200) - set(setMonash TRUE) - endif() - - add_custom_command(OUTPUT "${final_particle_list}" "${results_folder}/${i}/Afterburner/config.yaml" - COMMAND "${CMAKE_BINARY_DIR}/smash" - "-i" "${smash_afterburner_config}" - "-c" "Modi: { List: { File_Directory: ${sampler_dir}} }" - "-c" "General: { Nevents: ${N_events_afterburner} }" - "-c" "Collision_Term: { String_Parameters: {Use_Monash_Tune: ${setMonash} } }" - "-o" "${results_folder}/${i}/Afterburner" - "-f" - "-n" - ">" "/dev/null" - DEPENDS - "${smash_afterburner_config}" - "${CMAKE_BINARY_DIR}/smash" - "${full_particle_list}" - COMMENT "Running SMASH afterburner for custom hybrid run." - ) - - - #----------------------------------------------------------------------------# - # Analyse data - #----------------------------------------------------------------------------# - # Define filenames for particle spectra and create list of those - set(spectra_fnames "${results_folder}/${i}/Spectra/yspectra.txt" ; - "${results_folder}/${i}/Spectra/mtspectra.txt" ; - "${results_folder}/${i}/Spectra/ptspectra.txt" ; - "${results_folder}/${i}/Spectra/v2spectra.txt" ; - "${results_folder}/${i}/Spectra/meanmt0_midrapidity.txt" ; - "${results_folder}/${i}/Spectra/meanpt_midrapidity.txt" ; - "${results_folder}/${i}/Spectra/midrapidity_yield.txt" ; - "${results_folder}/${i}/Spectra/total_multiplicity.txt") - set(analysis_outputs "") - foreach(j ${spectra_fnames}) - list(APPEND analysis_outputs "${j}") - endforeach(j) - - list(APPEND all_analysis_outputs ${analysis_outputs}) - - # Perform analysis - add_custom_command(OUTPUT ${analysis_outputs} - COMMAND "python3" "${SMASH_ANALYSIS_PATH}/test/energy_scan/mult_and_spectra.py" - "--output_files" ${analysis_outputs} - "--input_files" "${final_particle_list}" - COMMENT "Analyzing spectra for ${system} @ ${energy} GeV (${i}/${num_runs})." - DEPENDS "${SMASH_ANALYSIS_PATH}/test/energy_scan/mult_and_spectra.py" - ) - - #----------------------------------------------------------------------------# - # Plot spectra - #----------------------------------------------------------------------------# - # Define plot names - set(plot_names "${results_folder}/${i}/Spectra/yspectra.pdf" ; - "${results_folder}/${i}/Spectra/mtspectra.pdf" ; - "${results_folder}/${i}/Spectra/ptspectra.pdf" ; - "${results_folder}/${i}/Spectra/v2spectra.pdf" ; - "${results_folder}/${i}/Spectra/meanmt0_midrapidity.pdf" ; - "${results_folder}/${i}/Spectra/meanpt_midrapidity.pdf" ; - "${results_folder}/${i}/Spectra/midrapidity_yield.pdf" ; - "${results_folder}/${i}/Spectra/total_multiplicity.pdf") - - set(plots "") - foreach(j ${plot_names}) - list(APPEND plots "${j}") - endforeach(j) - - list(APPEND all_plots ${plots}) - - # Perform plotting - add_custom_command(OUTPUT ${plots} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/plot_spectra.py" - "--input_files" "${results_folder}/${i}/Spectra/*.txt" - "--Nevents" "${N_events_afterburner}" - COMMENT "Plotting spectra for custom hybrid run." - ) - - endforeach(i) - - #----------------------------------------------------------------------------# - # Average previously obtained spectra - #----------------------------------------------------------------------------# - file(MAKE_DIRECTORY "${results_folder}/Averaged_Spectra") - set(averaged_spectra "${results_folder}/pT.txt" ; - "${results_folder}/mT.txt" ; - "${results_folder}/dNdy.txt" - ) - add_custom_command(OUTPUT ${averaged_spectra} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/average_spectra.py" - "--smash_ana_dir" "${SMASH_ANALYSIS_PATH}" - "--pT_files" "${results_folder}/*/Spectra/ptspectra.txt" - "--mT_files" "${results_folder}/*/Spectra/mtspectra.txt" - "--y_files" "${results_folder}/*/Spectra/yspectra.txt" - "--midyY_files" "${results_folder}/*/Spectra/midrapidity_yield.txt" - "--meanpT_files" "${results_folder}/*/Spectra/meanpt_midrapidity.txt" - "--v2_files" "${results_folder}/*/Spectra/v2spectra.txt" - "--Nevents" "${N_events_afterburner}" - "--output_dir" "${results_folder}/Averaged_Spectra/" - COMMENT "Averaging spectra for custom hybrid run." - ) - - #----------------------------------------------------------------------------# - # Plot averaged quantities - #----------------------------------------------------------------------------# - set(averaged_plots "${results_folder}/Averaged_Spectra/yspectra.pdf" ; - "${results_folder}/Averaged_Spectra/mtspectra.pdf" ; - "${results_folder}/Averaged_Spectra/ptspectra.pdf") - - # Perform plotting of averaged quantities - add_custom_command(OUTPUT ${averaged_plots} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/plot_spectra.py" - "--input_files" "${results_folder}/Averaged_Spectra/*.txt" - "--Nevents" "${N_events_afterburner}" - "--average" "True" - COMMENT "Plotting averaged spectra for custom hybrid run." - ) - - - # Define subtargets to enable separated running of the hybrid submodules - set(target custom_IC) - add_custom_target(${target} ALL DEPENDS ${smash_IC_files}) - - set(target custom_hydro) - add_custom_target(${target} ALL DEPENDS ${vhlle_freezeout_hypersurfaces}) - - set(target custom_sampler) - add_custom_target(${target} ALL DEPENDS ${sampled_particle_lists}) - - set(target custom_afterburner) - add_custom_target(${target} ALL DEPENDS ${final_particle_lists}) - - set(target custom_analysis) - add_custom_target(${target} ALL DEPENDS ${all_analysis_outputs}) - - set(target custom_plots) - add_custom_target(${target} ALL DEPENDS ${all_plots}) - - # Averaging - set(target custom_average_spectra) - add_custom_target(${target} ALL DEPENDS ${averaged_spectra}) - - set(target custom_average_plots) - add_custom_target(${target} ALL DEPENDS ${averaged_plots}) - -endfunction() - -run_custom_hybrid("15.0") - - -################################################################################ -# -# Add functionality for a low statistics test case -# This test case is not suitable for production runs, as quanutum number -# conservation cannot be guaranteed owing to the lack of precision. -# -################################################################################ -function(test_hybrid - energy_and_system - ) - list(GET energy_and_system 0 energy) - list(GET energy_and_system 1 system) - message(STATUS "Configuring hybrid test run, not intended for production runs.") - set(results_folder "${CMAKE_CURRENT_BINARY_DIR}/Hybrid_Results/test") - file(MAKE_DIRECTORY ${results_folder}) - - set(smash_IC_files "") - set(vhlle_freezeout_hypersurfaces "") - set(sampled_particle_lists "") - set(final_particle_lists "") - set(all_analysis_outputs "") - set(all_plots "") - - set(num_runs "4") - foreach(i RANGE 1 ${num_runs}) - file(MAKE_DIRECTORY "${results_folder}/${i}/IC") # subdir for initial conditions - file(MAKE_DIRECTORY "${results_folder}/${i}/Hydro") # subdir for hydro run - file(MAKE_DIRECTORY "${results_folder}/${i}/Sampler") # subdir for particle sampling - file(MAKE_DIRECTORY "${results_folder}/${i}/Afterburner") # subdir for afterburner - file(MAKE_DIRECTORY "${results_folder}/${i}/Spectra") # subdir for analyzed spectra and plots - - set(smash_IC_file "${results_folder}/${i}/IC/SMASH_IC.dat") - set(smash_IC_oscar "${results_folder}/${i}/IC/SMASH_IC.oscar") - set(smash_IC_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/smash_initial_conditions_AuAu.yaml") - set(vhlle_default_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/vhlle_hydro") - set(vhlle_output_dir "${results_folder}/${i}/Hydro/") - set(vhlle_updated_config "${results_folder}/${i}/Hydro/vhlle_config") - set(vhlle_freezeout_hypersurface "${results_folder}/${i}/Hydro/freezeout.dat") - set(sampler_input_hypersurface "${results_folder}/${i}/Sampler/freezeout.dat") - set(sampler_default_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/hadron_sampler") - set(sampler_updated_config "${results_folder}/${i}/Sampler/sampler_config") - set(sampler_dir "${results_folder}/${i}/Sampler") - set(sampled_particle_list "${results_folder}/${i}/Sampler/particle_lists.oscar") - set(full_particle_list "${results_folder}/${i}/Sampler/sampling0") - set(smash_afterburner_config "${CMAKE_CURRENT_SOURCE_DIR}/configs/smash_afterburner.yaml") - set(final_particle_list "${results_folder}/${i}/Afterburner/particles_binary.bin") - - list(APPEND smash_IC_files ${smash_IC_file}) - list(APPEND vhlle_freezeout_hypersurfaces ${vhlle_freezeout_hypersurface}) - list(APPEND sampled_particle_lists ${sampled_particle_list}) - list(APPEND final_particle_lists ${final_particle_list}) - - #----------------------------------------------------------------------------# - # Run SMASH and generate initial conditions output - #----------------------------------------------------------------------------# - add_custom_command(OUTPUT "${smash_IC_file}" "${results_folder}/${i}/IC/config.yaml" - COMMAND "${CMAKE_BINARY_DIR}/smash" - "-o" "${results_folder}/${i}/IC" - "-i" "${smash_IC_config}" - "-f" - "-n" - ">" "${results_folder}/${i}/IC/Terminal_Output.txt" - DEPENDS - "${smash_IC_config}" - "${CMAKE_BINARY_DIR}/smash" - COMMENT "Running SMASH for initial conditions of hybrid test run for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - - #----------------------------------------------------------------------------# - # Feed SMASH initial conditions into vHLLE and run hydro evolution - #----------------------------------------------------------------------------# - # create input file with correct paths for vHLLE - add_custom_command(OUTPUT "${vhlle_updated_config}" - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_vhlle_config.py" - "--vhlle_config" "${vhlle_default_config}" - "--smash_ic" "${smash_IC_file}" - "--output_file" "${vhlle_updated_config}" - "--test" - DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_vhlle_config.py" - "${vhlle_default_config}" - COMMENT "Creating input file for vHLLE for hybrid test run for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - - # Run hydro - add_custom_command(OUTPUT "${vhlle_freezeout_hypersurface}" - COMMAND "${CMAKE_BINARY_DIR}/hlle_visc" - "-params" "${vhlle_updated_config}" - "-ISinput" "${smash_IC_file}" - "-outputDir" "${vhlle_output_dir}" - ">" "${vhlle_output_dir}/Terminal_Output.txt" - DEPENDS - "${vhlle_updated_config}" - "${CMAKE_BINARY_DIR}/hlle_visc" - COMMENT "Running vHLLE for hybrid test run for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - #----------------------------------------------------------------------------# - # Run Cooper-Frye sampler for particlization of freezeout surface - #----------------------------------------------------------------------------# - # create input file with correct paths for sampler - set(N_events_afterburner "50") - add_custom_command(OUTPUT "${sampler_updated_config}" - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_sampler_config.py" - "--sampler_config" "${sampler_default_config}" - "--vhlle_config" "${vhlle_updated_config}" - "--output_file" "${sampler_updated_config}" - "--Nevents" "${N_events_afterburner}" - DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/create_sampler_config.py" - "${sampler_default_config}" - COMMENT "Creating input file for hybrid test run for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - # Run sampler - add_custom_command(OUTPUT ${sampled_particle_list} - COMMAND "${CMAKE_BINARY_DIR}/sampler" "events" "1" "${sampler_updated_config}" - ">" "${results_folder}/${i}/Sampler/Terminal_Output.txt" - DEPENDS - "${CMAKE_BINARY_DIR}/sampler" - "${sampler_updated_config}" - COMMENT "Sampling particles from freezeout hypersurface (${i}/${num_runs})." - ) - - - #----------------------------------------------------------------------------# - # Run SMASH as afterburner - #----------------------------------------------------------------------------# - # Add spectators to particle list and rename it to be in accordance - # with SMASH list modus input format - add_custom_command(OUTPUT ${full_particle_list} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/add_spectators.py" - "--sampled_particle_list" "${sampler_dir}/particle_lists.oscar" - "--initial_particle_list" "${smash_IC_oscar}" - "--output_file" "${full_particle_list}" - "--smash_config" "${results_folder}/${i}/IC/config.yaml" - DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/add_spectators.py" - COMMENT "Adding spectators to sampled particle list for afterburner (${i}/${num_runs})." - ) - - # Run afterburner evolution - add_custom_command(OUTPUT "${final_particle_list}" "${results_folder}/${i}/Afterburner/config.yaml" - COMMAND "${CMAKE_BINARY_DIR}/smash" - "-i" "${smash_afterburner_config}" - "-c" "Modi: { List: { File_Directory: ${sampler_dir}} }" - "-c" "General: { Nevents: ${N_events_afterburner} }" - "-o" "${results_folder}/${i}/Afterburner" - "-f" - "-n" - ">" "/dev/null" - DEPENDS - "${smash_afterburner_config}" - "${CMAKE_BINARY_DIR}/smash" - "${full_particle_list}" - COMMENT "Running SMASH afterburner for hybrid test run for ${system} @ ${energy} GeV (${i}/${num_runs})." - ) - - - #----------------------------------------------------------------------------# - # Analyse data - #----------------------------------------------------------------------------# - # Define filenames for particle spectra and create list of those - set(spectra_fnames "${results_folder}/${i}/Spectra/yspectra.txt" ; - "${results_folder}/${i}/Spectra/mtspectra.txt" ; - "${results_folder}/${i}/Spectra/ptspectra.txt" ; - "${results_folder}/${i}/Spectra/v2spectra.txt" ; - "${results_folder}/${i}/Spectra/meanmt0_midrapidity.txt" ; - "${results_folder}/${i}/Spectra/meanpt_midrapidity.txt" ; - "${results_folder}/${i}/Spectra/midrapidity_yield.txt" ; - "${results_folder}/${i}/Spectra/total_multiplicity.txt") - set(analysis_outputs "") - foreach(j ${spectra_fnames}) - list(APPEND analysis_outputs "${j}") - endforeach(j) - - list(APPEND all_analysis_outputs ${analysis_outputs}) - - # Perform analysis - add_custom_command(OUTPUT ${analysis_outputs} - COMMAND "python3" "${SMASH_ANALYSIS_PATH}/test/energy_scan/mult_and_spectra.py" - "--output_files" ${analysis_outputs} - "--input_files" "${final_particle_list}" - COMMENT "Analyzing spectra for ${system} @ ${energy} GeV (${i}/${num_runs})." - DEPENDS "${SMASH_ANALYSIS_PATH}/test/energy_scan/mult_and_spectra.py" - ) - - #----------------------------------------------------------------------------# - # Plot spectra - #----------------------------------------------------------------------------# - # Define plot names - set(plot_names "${results_folder}/${i}/Spectra/yspectra.pdf" ; - "${results_folder}/${i}/Spectra/mtspectra.pdf" ; - "${results_folder}/${i}/Spectra/ptspectra.pdf" ; - "${results_folder}/${i}/Spectra/v2spectra.pdf" ; - "${results_folder}/${i}/Spectra/meanmt0_midrapidity.pdf" ; - "${results_folder}/${i}/Spectra/meanpt_midrapidity.pdf" ; - "${results_folder}/${i}/Spectra/midrapidity_yield.pdf" ; - "${results_folder}/${i}/Spectra/total_multiplicity.pdf") - - set(plots "") - foreach(j ${plot_names}) - list(APPEND plots "${j}") - endforeach(j) - - list(APPEND all_plots ${plots}) - - # Perform plotting - add_custom_command(OUTPUT ${plots} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/plot_spectra.py" - "--input_files" "${results_folder}/${i}/Spectra/*.txt" - "--Nevents" "${N_events_afterburner}" - COMMENT "Plotting spectra for custom hybrid run." - ) - - endforeach(i) - - #----------------------------------------------------------------------------# - # Average previously obtained spectra - #----------------------------------------------------------------------------# - file(MAKE_DIRECTORY "${results_folder}/Averaged_Spectra") - set(averaged_spectra "${results_folder}/pT.txt" ; - "${results_folder}/mT.txt" ; - "${results_folder}/dNdy.txt" - ) - add_custom_command(OUTPUT ${averaged_spectra} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/average_spectra.py" - "--smash_ana_dir" "${SMASH_ANALYSIS_PATH}" - "--pT_files" "${results_folder}/*/Spectra/ptspectra.txt" - "--mT_files" "${results_folder}/*/Spectra/mtspectra.txt" - "--y_files" "${results_folder}/*/Spectra/yspectra.txt" - "--midyY_files" "${results_folder}/*/Spectra/midrapidity_yield.txt" - "--meanpT_files" "${results_folder}/*/Spectra/meanpt_midrapidity.txt" - "--v2_files" "${results_folder}/*/Spectra/v2spectra.txt" - "--Nevents" "${N_events_afterburner}" - "--output_dir" "${results_folder}/Averaged_Spectra/" - COMMENT "Averaging spectra for hybrid test run for ${system} @ ${energy} GeV." - ) - - #----------------------------------------------------------------------------# - # Plot averaged quantities - #----------------------------------------------------------------------------# - set(averaged_plots "${results_folder}/Averaged_Spectra/yspectra.pdf" ; - "${results_folder}/Averaged_Spectra/mtspectra.pdf" ; - "${results_folder}/Averaged_Spectra/ptspectra.pdf") - - # Perform plotting of averaged quantities - add_custom_command(OUTPUT ${averaged_plots} - COMMAND "python3" "${CMAKE_CURRENT_SOURCE_DIR}/python_scripts/plot_spectra.py" - "--input_files" "${results_folder}/Averaged_Spectra/*.txt" - "--Nevents" "${N_events_afterburner}" - "--average" "True" - COMMENT "Plotting averaged spectra for hybrid test run for ${system} @ ${energy} GeV." - ) - - - # Define subtargets to enable separated running of the hybrid submodules - set(target test_IC) - add_custom_target(${target} ALL DEPENDS ${smash_IC_files}) - - set(target test_hydro) - add_custom_target(${target} ALL DEPENDS ${vhlle_freezeout_hypersurfaces}) - - set(target test_sampler) - add_custom_target(${target} ALL DEPENDS ${sampled_particle_lists}) - - set(target test_afterburner) - add_custom_target(${target} ALL DEPENDS ${final_particle_lists}) - - set(target test_analysis) - add_custom_target(${target} ALL DEPENDS ${all_analysis_outputs}) - - set(target test_plots) - add_custom_target(${target} ALL DEPENDS ${all_plots}) - - # Averaging - set(target test_average_spectra) - add_custom_target(${target} ALL DEPENDS ${averaged_spectra}) - - set(target test_average_plots) - add_custom_target(${target} ALL DEPENDS ${averaged_plots}) - -endfunction() - -test_hybrid("7.7;AuAu") diff --git a/Hybrid-handler b/Hybrid-handler new file mode 100755 index 0000000..3e3a5b2 --- /dev/null +++ b/Hybrid-handler @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +#----------------------------------------------------------------------------# +# __ __ __ _ __ __ __ ____ # +# / / / /_ __/ /_ _____(_)___/ / / / / /___ _____ ____/ / /__ _____ # +# / /_/ / / / / __ \/ ___/ / __ / / /_/ / __ `/ __ \/ __ / / _ \/ ___/ # +# / __ / /_/ / /_/ / / / / /_/ / / __ / /_/ / / / / /_/ / / __/ / # +# /_/ /_/\__, /_.___/_/ /_/\__,_/ /_/ /_/\__,_/_/ /_/\__,_/_/\___/_/ # +# /____/ # +# # +#----------------------------------------------------------------------------# + +readonly HYBRID_codebase_version='SMASH-vHLLE-hybrid-1.0-unreleased' + +function Main() +{ + Enable_Desired_Shell_Behavior + Setup_Initial_And_Final_Output_Space + Define_Repository_Global_Path + Store_Command_Line_Options_Into_Global_Variable "$@" + Source_Codebase_Files + Define_Further_Global_Variables + Parse_Execution_Mode + Act_And_Exit_If_User_Ran_Auxiliary_Modes + Check_System_Requirements + Parse_Command_Line_Options + Make_Needed_Operations_Depending_On_Execution_Mode +} + +function Enable_Desired_Shell_Behavior() +{ + # Set stricter bash mode (see CONTRIBUTING for more information) + set -o pipefail -o nounset -o errexit + shopt -s extglob inherit_errexit +} + +function Setup_Initial_And_Final_Output_Space() +{ + printf '\n' + trap 'printf "\n"' EXIT +} + +function Define_Repository_Global_Path() +{ + # NOTE: This function would reduce to one line using 'readlink -f'. However this function + # is meant to be called very early and we want to make as few assumptions on the system + # as possible. On macOS the -f option has been added in macOS 12.3 (early 2022) and this + # might be problematic. Hence we basically only use 'readlink' to resolve possible + # symbolic links. Note that we require 'readlink' to exist as this was introduced in + # OpenBSD and GNU around 2000, see comments to https://unix.stackexchange.com/a/136527/370049. + if ! hash readlink &> /dev/null; then + exit_code=${HYBRID_fatal_builtin} Print_Fatal_And_Exit \ + "No 'readlink' command found. Please install it and run the Hybrid-handler again." + fi + local source directory + source=${BASH_SOURCE[0]} + while [[ -L "${source}" ]]; do # resolve ${source} until the file is no longer a symlink + # Find real directory following possible directory symlinks + directory=$(cd -P "$(dirname "${source}")" &> /dev/null && pwd) + source=$(readlink "${source}") + # NOTE: If ${source} was a relative symlink, we need to resolve it relative + # to the path where the symlink file was located. This is needed to ensure + # that readlink still works when the script is invoked from a different + # position from where it is located. + if [[ "${source}" != /* ]]; then + source=${directory}/${source} + fi + done + readonly HYBRID_top_level_path=$(cd -P "$(dirname "${source}")" &> /dev/null && pwd) +} + +function Store_Command_Line_Options_Into_Global_Variable() +{ + HYBRID_command_line_options_to_parse=("$@") +} + +function Source_Codebase_Files() +{ + source "${HYBRID_top_level_path}/bash/source_codebase_files.bash" || exit 1 +} + +function Act_And_Exit_If_User_Ran_Auxiliary_Modes() +{ + case "${HYBRID_execution_mode}" in + *help) + Give_Required_Help + ;;& + format) + Format_Codebase + Run_Formatting_Unit_Test + ;;& + version) + Print_Software_Version + ;;& + *help | format | version) + exit ${HYBRID_success_exit_code} + ;; + esac +} + +function Make_Needed_Operations_Depending_On_Execution_Mode() +{ + case "${HYBRID_execution_mode}" in + do | prepare-scan) + Perform_Internal_Sanity_Checks + Validate_And_Parse_Configuration_File + Perform_Sanity_Checks_On_Provided_Input_And_Define_Auxiliary_Global_Variables + ;;& + do) + Do_Needed_Operations_For_Given_Software + ;; + prepare-scan) + Do_Needed_Operation_For_Parameter_Scan + ;; + *) + Print_Internal_And_Exit "Unexpected execution mode at top-level." + ;; + esac +} + +Main "$@" diff --git a/LICENSE b/LICENSE index 0cd9e98..f288702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,4 @@ - -This document contains the two licenses currently in use for the smash-vhlle-hybrid. -GPLv3 for all python and bash scripts -BSD 3-clause for cmake scripts - - GNU GENERAL PUBLIC LICENSE + GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. @@ -677,37 +672,3 @@ may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . - - - -######################################################################## - -Cmake BSD 3-clause - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -* Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. - -* Neither the names of Kitware, Inc., the Insight Software Consortium, -nor the names of their contributors may be used to endorse or promote -products derived from this software without specific prior written -permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 0ae1a4b..935ccb9 100644 --- a/README.md +++ b/README.md @@ -1,205 +1,19 @@ # SMASH-vHLLE-Hybrid -Event-by-event hybrid model for the description of relativistic heavy-ion collisions in the low and high baryon-density regime. This model constitutes a chain of different submodules to appropriately describe each phase of the collision with its corresponding degrees of freedom. It consists of the following modules: -- SMASH hadronic transport approach to provide the initial conditions -- vHLLE 3+1D viscous hydrodynamics approach to describe the evolution of the hot and dense fireball -- CORNELIUS tool to construct a hypersurface of constant energy density from the hydrodynamical evolution (embedded in vHLLE) -- Cooper-Frye sampler to perform particlization of the elements on the freezeout hypersurface -- SMASH hadronic transport approach to perform the afterburner evolution -If you are using the SMASH-vHLLE-hybrid, please cite [arXiv:2112.08724](https://arxiv.org/abs/2112.08724). You may also consult this reference for further details about the hybrid approach. + -## Prerequisites -- [cmake](https://cmake.org) version ≥ 3.15.4 -- [SMASH](https://github.com/smash-transport/smash) version ≥ 1.8 -- [vHLLE](https://github.com/yukarpenko/vhlle) -- [vHLLE parameters](https://github.com/yukarpenko/vhlle_params) -- [hadron sampler](https://github.com/smash-transport/smash-hadron-sampler) version ≥ 1.0 -- python version ≥ 2.7 -- ([SMASH-analysis](https://github.com/smash-transport/smash-analysis) version ≥ 1.7, if automatic generation of particle spectra is desired) +This project is in a transition phase. +A website associated to this project with full documentation guides will be deployed soon. +For the moment you can serve it locally in few steps: -Before building the full hybrid model, please make sure that the submodules listed above (SMASH, vHLLE, vHLLE params, sampler) are available and already compiled. Instructions on how to compile them can be found in the corresponding READMEs. +1. Get MkDocs and the used theme (once only, of course) + ``` + pip install mkdocs + pip install mkdocs-material + ``` +2. Run `mkdocs serve` from the top-level folder of this repository in your terminal +3. Open up [http://127.0.0.1:8000/](http://127.0.0.1:8000/) in your favorite browser -The newer versions of ROOT require C++17 bindings or higher, so please make sure to compile SMASH, ROOT, and the sampler with the same compiler utilizing the same compiler flags, which can be adjusted in CMakeLists.txt of each submodule. It is also recommended to start from a clean build directory whenever changing the compiler or linking to external libraries that were compiled with different compiler flags. +## LICENSE -## Building the hybrid model - -Once the prerequisites are met, use the following commands to build the full hybrid model in a separate `build` directory: - - mkdir build - cd build - cmake .. -DSMASH_PATH=[...]/smash/build -DVHLLE_PATH=[...]/vhlle -DVHLLE_PARAMS_PATH=[...]/vhlle_params/ -DSAMPLER_PATH=[...]/smash-hadron-sampler/build - -where `[...]` denote the paths to the `smash/build` directory, the `vhlle` directory, the `vhlle_params` directory and the `smash-hadron-sampler/build` directory. The binaries of the precompiled submodules are expected to be located therein. The `vhlle_params` directory does not contain any binary though, it only holds the equations of state necessary for the hydrodynamic evolution. - -All subtargets corresponding to the predefined collision setups have been created by `cmake`. To more easily divide the full hybrid run into smaller pieces, different targets are created for each step of the hybrid simulation. They have to be run one after the other in the order specified below. For a Pb+Pb collision at sqrt(s) = 8.8 GeV, this chain is executed via: - - make PbPb_8.8_IC - make PbPb_8.8_hydro - make PbPb_8.8_sampler - make PbPb_8.8_afterburner - -The output files of the individual submodules as well as the configuration files used can be found in the newly-created directory `[...]/build/Hybrid_Results/PbPb_8.8GeV/i`, where `i` corresponds to the i-th hybrid run in an event-by-event setup. By default, the full hybrid model is run 100 times in parallel for different initial states. To change the number of parallel runs, modify the parameter `num_runs` in `CMakeLists.txt`. - -## Building the hybrid model linked to the SMASH-analysis -To also provide the automatic analysis of the final particle lists, run the following commands to link the project to the smash-analysis: - - mkdir build - cd build - cmake .. -DSMASH_PATH=[...]/smash/build -DVHLLE_PATH=[...]/vhlle -DVHLLE_PARAMS_PATH=[...]/vhlle_params/ -DSAMPLER_PATH=[...]/smash-hadron-sampler/build -DSMASH_ANALYSIS_PATH=[...]/smash-analysis - -Once the afterburner was run, the resulting particle lists can be analysed and plotted by executing: - - make PbPb_8.8_analysis - make PbPb_8.8_plots - -The generated plots and output files are then located in `[...]/build/Hybrid_Results/PbPb_8.8GeV/i/Spectra`. The above commands analyse and plot the results of each of the 100 parallel hybrid runs. It is useful to average over the obtained results to obtain averaged final-state plots. This is done by executing - - make PbPb_8.8_average_spectra - make PbPb_8.8_average_plots - -in this specific order. The final output files, that is tables and plots, are then located in `[...]/build/Hybrid_Results/PbPb_8.8GeV/Averaged_Spectra`. - -In addition, it is possible to extract excitation functions for the mean transverse momentum and the mid-rapidity yield if the SMASH-vHLLE-hybrid was run for different collision energies. To obtain those, execute - - make exc_funcs - -after having analyzed and averaged the output at all collision energies with the above stated commands. The resulting excitation functions in terms of tables and plots containing entries at all previously run collision energies are then located in `[...]/build/Hybrid_Results`. - -## Configuring the collision setups -A number of different collision setups for the hybrid model are supported out of the box. The shear viscosities applied are taken from *Karpenko et al.: Phys.Rev.C 91 (2015)* and the longitudinal and transversal smearing parameters are adjusted to improve agreement with experimental data. The supported collision systems are: -* AuAu @ sqrt(s) = 4.3 GeV -* PbPb @ sqrt(s) = 6.4 GeV -* AuAu @ sqrt(s) = 7.7 GeV -* PbPb @ sqrt(s) = 8.8 GeV -* PbPb @ sqrt(s) = 17.3 GeV -* AuAu @ sqrt(s) = 27.0 GeV -* AuAu @ sqrt(s) = 39.0 GeV -* AuAu @ sqrt(s) = 62.4 GeV -* AuAu @ sqrt(s) = 130.0 GeV -* AuAu @ sqrt(s) = 200.0 GeV -* PbPb @ sqrt(s) = 2.76 TeV -* PbPb @ sqrt(s) = 5.02 TeV - -They can be executed in analogy to the PbPb @ sqrt(s) = 8.8 GeV example presented above. - -To run additional setups it is necessary to add the corresponding targets to the bottom of the `CMakeLists.txt` file.
-The configuration files are located in `[...]/configs/`. Four different configuration files are necessary to run the full hybrid model. These are: -1. `smash_initial_conditions_AuAu.yaml` or `smash_initial_conditions_PbPb.yaml` for the configuration of the initial setup of the collision. There are two initial conditions files corresponding to collision systems of Au+Au and Pb+Pb, respectively. If additional collision systems are desired, it is necessary to add an appropriate configuration file to the `[...]/configs/` directory. For details and further information about the configuration of SMASH, consult the [SMASH User Guide](http://theory.gsi.de/~smash/userguide/current/). -2. `vhlle_hydro` for the configuration of the hydrodynamic evolution.
-Further information about the configuration of vHLLE is provided in the `README.txt` of vHLLE. -3. `hadron_sampler` for the configuration of the sampler.
-Further information about the configuration of the sampler is provided in the `README.md` of the `hadron-sampler`. -4. `smash_afterburner.yaml` for the configuration of the SMASH afterburner evolution based on the sampled particle list. - -**Note:** In all configuration files, the paths to input files from the previous stages are adjusted automatically. Also cross-parameters that require consistency between the hydrodynamics evolution and the sampler, e.g. viscosities and critical energy density, are taken care of automatically. - -## Running a test setup - -The default setup of the SMASH-vHLLE-hybrid is meant to be executed on a computing cluster. To test the functionality and to also run it on a local computer, there is the possibility to execute a test setup of the SMASH-vHLLE-hybrid, which is a Au+Au collision at sqrt(s) = 7.7 GeV. The statistics are significantly reduced however and the grid for the hydrodynamic evolution is characterized by large cells, too large for a realistic simulation scenario.
-**This test setup is hence only meant to be used in order to test the functionality of the framework and the embedded scripts, but not for production runs.** - -To execute the different stages of the test setup, run - - make test_IC - make test_hydro - make test_sampler - make test_afterburner - -If the SMASH-vHLLE-hybrid is linked to the SMASH-analysis, one can additionally test the analysis scripts by executing - - make test_analysis - make test_average_spectra - make test_average_plots - -The results are then located in `[...]/build/Hybrid_Results/test`. - - -## Using a custom SMASH configuration file for the initial conditions - -In addition to the above described predefined Au+Au and Pb+Pb collisions, it is possible to employ a custom `SMASH` configuration file in the initial stage. This file is expected to be located in the `[...]/configs/` directory and named `smash_initial_conditions_custom.yaml`. An example configuration is shipped with the `SMASH-vHLLE-hybrid`, it can be modified as desired. Among the essential parameters the collision system and impact parameter range can be adjusted in the configuration file `smash_initial_conditions_custom.yaml`, while the collision energy needs to be specified in the file `CMakeLists.txt` as an argument of the function `run_custom_hybrid(energy)`. To run the hybrid with this custom configuration file, execute the following commands: - - make custom_IC - make custom_hydro - make custom_sampler - make custom_afterburner - -The results are stored in `[...]/build/Hybrid_Results/Custom` and the subdirectories are structured identically as for the predefined Au+Au or Pb+Pb collisions as described above. - -To further analyze and average the outcome, if the `SMASH-vHLLE-hybrid` is coupled to the `SMASH-analysis` as described above, one may further execute - - make custom_analysis - make custom_average_spectra - make custom_average_plots - -to obtain rapidity and transverse mass spectra that are stored in `[...]/build/Hybrid_Results/Custom/Averaged_Spectra`. - -**Note:** -When using the custom configuration, it is possible to specify the viscosity and smearing parameters employed in the hydrodynamical evolution in `python_scripts/hydro_parameters.py` file, by adding or editing an entry in the `hydro_params` dictionary, where the collision energy is the key. By default for a defined collision energy the values in the dictionary are used, while for all other energies parameter values of the nearest lower energy are taken. - -# Module exchanges and further modifications -It might be desired to run the SMASH-vHLLE-hybrid, but relying on a different initial state, particle sampler or similar. For this, the `CMakeLists.txt` need to be updated accordingly. Exemplary instructions for a number of use cases are provided in the following. - -#### Running vHLLE on an averaged initial state from SMASH -By default, the SMASH-vHLLE-hybrid performs event-by-event simulations. To run a single-shot hydrodynamical event with initial conditions obtained from multiple averaged SMASH events, you can proceed as follows: -1. Open the configuration file for the SMASH initial state: `configs/smash_initial_conditions_AuAu.yaml` and/or `configs/smash_initial_conditions_PbPb.yaml`. -2. Update the event counter (`Nevents`) with the number of events you wish to average over for the initial state. -3. Open the file `CMakeLists.txt` and set the `num_runs` parameter to 1. -4. **Caveat:** For the moment, the addition of spectators to the sampled particle list for the afterburner evolution is not implemented in the case of averaged initial conditions. In the current state, the totality of spectators extracted from all initial SMASH events would be added to each sampled afterburner event. This is of course wrong. For an approximation in central events it might be sufficient to neglect those spectators. This can be achieved by replacing line 97 in `python_scripts/add_spectators.py`: `write_full_particle_list(spectators)` by `write_full_particle_list([])`.
-Alternatively, one could modify SMASH in order to print participants as well as spectators to the output file that is used as input in vHLLE, such that they also contribute to the averaged initial state. For this, open the SMASH source file `smash/src/icoutput.cc` and navigate to the function `void ICOutput::at_interaction`. Therein, the statement -```cpp -if (!is_spectator) { - std::fprintf(file_.get(), "%g %g %g %g %g %g %g %g %s %i \n", - particle.position().tau(), particle.position()[1], - particle.position()[2], particle.position().eta(), m_trans, - particle.momentum()[1], particle.momentum()[2], rapidity, - particle.pdgcode().string().c_str(), particle.type().charge()); -} -``` -controls that spectators are not printed to the output. Simply remove the if statement and the corresponding brackets to print all particles to the output. Make sure to re-compile SMASH after this modification and before linking to the hybrid. Additionally, one also needs to replace line 97 in `python_scripts/add_spectators.py`: `write_full_particle_list(spectators)` by `write_full_particle_list([])` to not add the spectators twice. -4. Run `cmake ..` in the build directory. -5. Proceed as usual to run the different steps as described above. - -#### Using different kinds of initial condition that are supported by vHLLE -By default, the SMASH-vHLLE-hybrid relies on an initial state from SMASH. In vHLLE however, a number of different initial states are supported. To make use of them, proceed as follows: -1. Open the vHLLE configuration file in `configs/vhlle_hydro`. -2. Update the parameter `icModel` with the number corresponding to the initial state you wish to use. For further information about potential IC models, please consult [vHLLE](https://github.com/yukarpenko/vhlle). -3. If vHLLE requires an external file for the initialization, update line 170 in `CMakeLists.txt`: -```cmake -"-ISinput" "${smash_IC_file}" -``` -by replacing the `smash_IC_file` by whichever file you want to use. It might make sense to define a new parameter (similar to `smash_IC_file`) with the path to the respective file, as done for the `smash_IC_file` in line 112. If no external file is required, simply remove/comment line 170. -4. Run `cmake ..` in the build directory. -5. Proceed as usual to run the different steps as described above. - -#### Using a different particle sampler -By default, the SMASH-vHLLE-hybrid relies on the SMASH-hadron-sampler for the particlization process. If desired, this sampler can be exchanged by any other. It is however necessary that the created output fulfills the requirement from SMASH for the afterburner evolution. Information about those requirements is provided in the [SMASH user guide](http://theory.gsi.de/~smash/userguide/2.0.2/input_modi_list_.html). -The commands to produce the configuration file and execute the particle sampler are located in lines 183-204 of `CMakeLists.txt`. To exchange this sampler, you may proceed as follows: -1. First, the script `python_scripts/create_sampler_config.py` is executed to create the configuration file which is needed to initialize the sampler execution. If the alternative sampler also requires such a configuration file, you should update the python script accordingly. -2. Second, the sampler itself is executed. This is achieved with the following command in lines 198-204: -```cmake -add_custom_command(OUTPUT ${sampled_particle_list} -COMMAND "${CMAKE_BINARY_DIR}/sampler" "events" "1" "${sampler_updated_config}" - ">" "${results_folder}/${i}/Sampler/Terminal_Output.txt" -DEPENDS - "${CMAKE_BINARY_DIR}/sampler" - "${sampler_updated_config}" -COMMENT "Sampling particles from freezeout hypersurface (${i}/${num_runs})." -) -``` -where the lines following 'COMMAND' correspond to whatever one would type in the terminal in order to execute the sampler from there. Note though that ever piece of this command needs to be surrounded by quotation marks within the CMake script. -The 'OUTPUT' command is followed by the path to the output file that is to be created from the sampler. It usually corresponds to the sampled particle list. Whatever follows 'DEPENDS' are those files/scripts on which the sampler relies. More concretely, this means that the sampler is re-executed if any of the files/scripts mentioned therein are changed. Finally, 'COMMENT' defines a corresponding comment that is printed to the terminal upon execution of the sampler.
-To exchange the sampler by a different one, the user may exchange the lines described above by a custom command targeted at executing a different sampler. -3. The sampled particle list is further combined with the spectators, that where not plugged into the hydro evolution in the first place. This is achieved with the script `python_scripts/add_spectators.py`. If desired, update the cmake command in lines 211-219 accordingly, to rely on the sampled particle list obtained from the new sampler. -4. If a combination with spectators is not desired, the command mentioned under 3 can simply be removed. In that case however the path to the particle list that is read in from SMASH for the afterburner evolution needs to be exchanged by the path to the directory, where the list produced with the new sampler is located. This can be achieved by updating line 225 `"-c" "Modi: { List: { File_Directory: ${sampler_dir}} }"` and replacing the variable `${sampler_dir}` with the correct path. Furthermore, the name of the output file (if not identical to the default, which is `sampling0` within the SMASH-vHLLE-hybrid), needs to be updated in the SMASH afterburner config `configs/smash_afterburner.yaml`: -```yaml -Modi: - List: - File_Directory: "../build/" - File_Prefix: "sampling" - Shift_Id: 0 -``` - - Note that SMASH expects every file to terminate with a number (see [SMASH user guide](http://theory.gsi.de/~smash/userguide/current/) for further details and background), so make sure to name the file accordingly. If the filename does not end with `0`, you need to update the parameter `Shift_Id` accordingly. The prefix of the file name (that is everything except the number) is controlled by the `File_Prefix` argument and may also require being updated. The `File_Directory` is updated automatically from within the `CMakeLists.txt`. - -4. Run `cmake ..` in the build directory and make sure it configures without errors. -5. Proceed as usual to run the different steps as described above. +Apart from the project logo, for which [CC-BY-ND](https://creativecommons.org/licenses/by-nd/4.0/) applies, the rest of the codebase is licensed under [GPLv3](LICENSE). diff --git a/bash/Afterburner_functionality.bash b/bash/Afterburner_functionality.bash new file mode 100644 index 0000000..55784a0 --- /dev/null +++ b/bash/Afterburner_functionality.bash @@ -0,0 +1,142 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Prepare_Software_Input_File_Afterburner() +{ + __static__Ensure_Consistency_Of_Afterburner_Input + Create_Output_Directory_For 'Afterburner' + Ensure_Given_Files_Do_Not_Exist "${HYBRID_software_configuration_file[Afterburner]}" + Ensure_Given_Files_Exist "${HYBRID_software_base_config_file[Afterburner]}" + Copy_Base_Configuration_To_Output_Folder_For 'Afterburner' + Replace_Keys_In_Configuration_File_If_Needed_For 'Afterburner' + __static__Create_Sampled_Particles_List_File_Or_Symbolic_Link_With_Or_Without_Spectators + __static__Check_If_Afterburner_Configuration_Is_Consistent_With_Sampler +} + +function Ensure_All_Needed_Input_Exists_Afterburner() +{ + Ensure_Given_Folders_Exist "${HYBRID_software_output_directory[Afterburner]}" + Ensure_Input_File_Exists_And_Alert_If_Unfinished \ + "${HYBRID_software_input_file[Afterburner]}" + Ensure_Given_Files_Exist \ + "${HYBRID_software_configuration_file[Afterburner]}" + Internally_Ensure_Given_Files_Exist \ + "${HYBRID_software_output_directory[Afterburner]}/${HYBRID_afterburner_list_filename}" +} + +function Ensure_Run_Reproducibility_Afterburner() +{ + Copy_Hybrid_Handler_Config_Section 'Afterburner' \ + "${HYBRID_software_output_directory[Afterburner]}" \ + "$(dirname "$(realpath "${HYBRID_software_executable[Afterburner]}")")" +} + +function Run_Software_Afterburner() +{ + Separate_Terminal_Output_For 'Afterburner' + cd "${HYBRID_software_output_directory[Afterburner]}" + "${HYBRID_software_executable[Afterburner]}" \ + '-i' "${HYBRID_software_configuration_file[Afterburner]}" \ + '-o' "${HYBRID_software_output_directory[Afterburner]}" \ + '-n' \ + &>> "${HYBRID_software_output_directory[Afterburner]}/${HYBRID_terminal_output[Afterburner]}" \ + || Report_About_Software_Failure_For 'Afterburner' +} + +#=================================================================================================== + +function __static__Ensure_Consistency_Of_Afterburner_Input() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty 'HYBRID_software_input_file[Afterburner]' + if Has_YAML_String_Given_Key \ + "$(< "${HYBRID_configuration_file}")" 'Afterburner' 'Software_keys' 'Modi' 'List' 'Filename'; then + local given_filename + given_filename=$(Read_From_YAML_String_Given_Key "$(< "${HYBRID_configuration_file}")" 'Afterburner' \ + 'Software_keys' 'Modi' 'List' 'Filename') + if [[ "${given_filename}" != "${HYBRID_software_input_file[Afterburner]}" ]]; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'The Afterburner input particle list has to be modified via the ' \ + --emph 'Input_file' ' key,' 'not the ' --emph 'Software_keys' \ + ' specifying the input list filename!' + fi + fi + if Has_YAML_String_Given_Key \ + "$(< "${HYBRID_configuration_file}")" 'Afterburner' 'Software_keys' 'Modi' 'List' 'Shift_ID' \ + || Has_YAML_String_Given_Key \ + "$(< "${HYBRID_configuration_file}")" 'Afterburner' 'Software_keys' 'Modi' 'List' 'File_Prefix'; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'The Afterburner input particle list has to be modified via the ' \ + --emph 'Input_file' ' key,' 'not the ' --emph 'Software_keys' \ + ' specifying the input list prefix and ID!' + fi +} + +function __static__Create_Sampled_Particles_List_File_Or_Symbolic_Link_With_Or_Without_Spectators() +{ + local -r target_link_name="${HYBRID_software_output_directory[Afterburner]}/${HYBRID_afterburner_list_filename}" + if [[ "${HYBRID_optional_feature[Add_spectators_from_IC]}" = 'TRUE' ]]; then + Ensure_Given_Files_Do_Not_Exist "${target_link_name}" + # Here the config.yaml file is expected to be produced by SMASH in the output folder + # during the IC run. It is used to determine the initial number of particles. + Ensure_Given_Files_Exist \ + 'This file is expected to be produced by the IC software run' \ + 'and is needed to check number of initial nucleons.' '--' \ + "${HYBRID_software_output_directory[IC]}/config.yaml" + Ensure_Given_Files_Exist \ + "${HYBRID_software_input_file[Afterburner]}" \ + "${HYBRID_software_input_file[Spectators]}" + # Run Python script to add spectators + "${HYBRID_external_python_scripts[Add_spectators_from_IC]}" \ + '--sampled_particle_list' "${HYBRID_software_input_file[Afterburner]}" \ + '--initial_particle_list' "${HYBRID_software_input_file[Spectators]}" \ + '--output_file' "${target_link_name}" \ + '--smash_config' "${HYBRID_software_output_directory[IC]}/config.yaml" + else + if [[ ! -f "${target_link_name}" || -L "${target_link_name}" ]]; then + ln -s -f "${HYBRID_software_input_file[Afterburner]}" "${target_link_name}" + elif [[ ! "${target_link_name}" -ef "${HYBRID_software_input_file[Afterburner]}" ]]; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'File ' --emph "${target_link_name}" ' exists but it is not the Afterburner input file ' \ + --emph "${HYBRID_software_input_file[Afterburner]}" ' to be used.' + fi + fi +} + +function __static__Check_If_Afterburner_Configuration_Is_Consistent_With_Sampler() +{ + local -r config_afterburner="${HYBRID_software_configuration_file[Afterburner]}" + if Element_In_Array_Equals_To 'Sampler' "${HYBRID_given_software_sections[@]}"; then + local -r config_sampler="${HYBRID_software_configuration_file[Sampler]}" + while read key value; do + if [[ "${key}" = 'number_of_events' ]]; then + local events_sampler + events_sampler="${value}" + fi + done < "${config_sampler}" + local events_afterburner + events_afterburner=$(Read_From_YAML_String_Given_Key "$(< "${config_afterburner}")" 'General.Nevents') + if [[ "${events_afterburner}" -gt "${events_sampler}" ]]; then + PrintAttention 'The number of events set to run in the afterburner (' \ + --emph "${events_afterburner}" ')\nis greater than the number of events sampled (' \ + --emph "${events_sampler}" ').\n' \ + --emph 'Nevents' ' in the afterburner configuration file is reset to ' --emph "${events_sampler}" '!' + Remove_Comments_And_Replace_Provided_Keys_In_Provided_Input_File \ + 'YAML' "${config_afterburner}" \ + "$(printf "%s:\n %s: %s\n" 'General' 'Nevents' "${events_sampler}")" + elif [[ "${events_afterburner}" -lt "${events_sampler}" ]]; then + PrintAttention 'The number of events set to run in the afterburner (' \ + --emph "${events_afterburner}" ')\nis smaller than the number of events sampled (' \ + --emph "${events_sampler}" ').' \ + 'Excess sampled events remain unused.' \ + 'Please, ensure that this is desired behavior.' + fi + fi +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/Hydro_functionality.bash b/bash/Hydro_functionality.bash new file mode 100644 index 0000000..84e190d --- /dev/null +++ b/bash/Hydro_functionality.bash @@ -0,0 +1,105 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Prepare_Software_Input_File_Hydro() +{ + Create_Output_Directory_For 'Hydro' + Ensure_Given_Files_Do_Not_Exist "${HYBRID_software_configuration_file[Hydro]}" + Ensure_Given_Files_Exist "${HYBRID_software_base_config_file[Hydro]}" + Copy_Base_Configuration_To_Output_Folder_For 'Hydro' + Replace_Keys_In_Configuration_File_If_Needed_For 'Hydro' + __static__Create_Symbolic_Link_To_IC_File + __static__Create_Symbolic_Link_To_EOS_Folder +} + +function Ensure_All_Needed_Input_Exists_Hydro() +{ + Ensure_Given_Folders_Exist "${HYBRID_software_output_directory[Hydro]}" + Ensure_Input_File_Exists_And_Alert_If_Unfinished \ + "${HYBRID_software_input_file[Hydro]}" + Ensure_Given_Files_Exist \ + "${HYBRID_software_configuration_file[Hydro]}" + Internally_Ensure_Given_Files_Exist "${HYBRID_software_output_directory[Hydro]}/SMASH_IC.dat" +} + +function Ensure_Run_Reproducibility_Hydro() +{ + Copy_Hybrid_Handler_Config_Section 'Hydro' \ + "${HYBRID_software_output_directory[Hydro]}" \ + "$(dirname "$(realpath "${HYBRID_software_executable[Hydro]}")")" +} + +function Run_Software_Hydro() +{ + Separate_Terminal_Output_For 'Hydro' + cd "${HYBRID_software_output_directory[Hydro]}" + local -r \ + hydro_config_file_path="${HYBRID_software_configuration_file[Hydro]}" \ + ic_output_file_path="${HYBRID_software_output_directory[Hydro]}/SMASH_IC.dat" + "${HYBRID_software_executable[Hydro]}" \ + "-params" "${hydro_config_file_path}" \ + "-ISinput" "${ic_output_file_path}" \ + "-outputDir" "${HYBRID_software_output_directory[Hydro]}" &>> \ + "${HYBRID_software_output_directory[Hydro]}/${HYBRID_terminal_output[Hydro]}" \ + || Report_About_Software_Failure_For 'Hydro' +} + +#=================================================================================================== + +function __static__Create_Symbolic_Link_To_IC_File() +{ + local -r target_link_name="${HYBRID_software_output_directory[Hydro]}/SMASH_IC.dat" + if [[ ! -f "${target_link_name}" || -L "${target_link_name}" ]]; then + ln -s -f "${HYBRID_software_input_file[Hydro]}" "${target_link_name}" + elif [[ ! "${target_link_name}" -ef "${HYBRID_software_input_file[Hydro]}" ]]; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'File ' --emph "${target_link_name}" ' exists but it is not the Hydro input file ' \ + --emph "${HYBRID_software_input_file[Hydro]}" ' to be used.' + fi +} + +# NOTE: The 'eos' folder is assumed to exist in the hydro software folder. +# The user-specified software executable is guaranteed to be either a +# command name or a global path and in both cases 'type -P' is expected +# to succeed and print a global path. +function __static__Create_Symbolic_Link_To_EOS_Folder() +{ + local eos_folder + eos_folder="$(dirname $(type -P "${HYBRID_software_executable[Hydro]}"))/eos" + if [[ ! -d "${eos_folder}" ]]; then + exit_code=${HYBRID_fatal_file_not_found} Print_Fatal_And_Exit \ + 'The folder ' --emph "${eos_folder}" ' does not exist.' + fi + local -r link_to_eos_folder="${HYBRID_software_output_directory[Hydro]}/eos" + if [[ -d "${link_to_eos_folder}" ]]; then + if [[ ! "${link_to_eos_folder}" -ef "${eos_folder}" ]]; then + if [[ -L "${link_to_eos_folder}" ]]; then + Print_Warning \ + 'Found a symlink ' --emph "${HYBRID_software_output_directory[Hydro]}/eos" \ + '\npointing to a different eos folder. Unlink and link again!\n' + unlink "${HYBRID_software_output_directory[Hydro]}/eos" + ln -s "${eos_folder}" "${link_to_eos_folder}" + else + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'A ' --emph 'eos' ' folder called already exists at ' \ + --emph "${HYBRID_software_output_directory[Hydro]}" \ + '.' 'Please remove it and run the hybrid handler again.' + fi + fi + elif [[ -e "${link_to_eos_folder}" ]]; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'A ' --emph 'eos' ' file already exists at ' \ + --emph "${HYBRID_software_output_directory[Hydro]}" \ + '.' 'Please remove it and run the hybrid handler again.' + else + ln -s "${eos_folder}" "${link_to_eos_folder}" + fi +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/IC_functionality.bash b/bash/IC_functionality.bash new file mode 100644 index 0000000..af4d9d4 --- /dev/null +++ b/bash/IC_functionality.bash @@ -0,0 +1,44 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Prepare_Software_Input_File_IC() +{ + Create_Output_Directory_For 'IC' + Ensure_Given_Files_Do_Not_Exist "${HYBRID_software_configuration_file[IC]}" + Ensure_Given_Files_Exist "${HYBRID_software_base_config_file[IC]}" + Copy_Base_Configuration_To_Output_Folder_For 'IC' + Replace_Keys_In_Configuration_File_If_Needed_For 'IC' +} + +function Ensure_All_Needed_Input_Exists_IC() +{ + Ensure_Given_Folders_Exist "${HYBRID_software_output_directory[IC]}" + Ensure_Given_Files_Exist "${HYBRID_software_configuration_file[IC]}" +} + +function Ensure_Run_Reproducibility_IC() +{ + Copy_Hybrid_Handler_Config_Section 'IC' \ + "${HYBRID_software_output_directory[IC]}" \ + "$(dirname "$(realpath "${HYBRID_software_executable[IC]}")")" +} + +function Run_Software_IC() +{ + Separate_Terminal_Output_For 'IC' + cd "${HYBRID_software_output_directory[IC]}" + "${HYBRID_software_executable[IC]}" \ + '-i' "${HYBRID_software_configuration_file[IC]}" \ + '-o' "${HYBRID_software_output_directory[IC]}" \ + '-n' \ + &>> "${HYBRID_software_output_directory[IC]}/${HYBRID_terminal_output[IC]}" \ + || Report_About_Software_Failure_For 'IC' +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/Sampler_functionality.bash b/bash/Sampler_functionality.bash new file mode 100644 index 0000000..f23e6d5 --- /dev/null +++ b/bash/Sampler_functionality.bash @@ -0,0 +1,300 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Prepare_Software_Input_File_Sampler() +{ + Create_Output_Directory_For 'Sampler' + Ensure_Given_Files_Do_Not_Exist "${HYBRID_software_configuration_file[Sampler]}" + Ensure_Given_Files_Exist "${HYBRID_software_base_config_file[Sampler]}" + Copy_Base_Configuration_To_Output_Folder_For 'Sampler' + Replace_Keys_In_Configuration_File_If_Needed_For 'Sampler' + __static__Validate_Sampler_Config_File + __static__Check_If_Sampler_Configuration_Is_Consistent_With_Hydro + __static__Transform_Relative_Paths_In_Sampler_Config_File + __static__Create_Superfluous_Symbolic_Link_To_Freezeout_File_Ensuring_Its_Existence +} + +function Ensure_All_Needed_Input_Exists_Sampler() +{ + Ensure_Given_Folders_Exist "${HYBRID_software_output_directory[Sampler]}" + Ensure_Given_Files_Exist "${HYBRID_software_configuration_file[Sampler]}" + # This is already done preparing the input file, but it's logically belonging here. + # Therefore, we repeat the validation, as its cost is substantially negligible. + __static__Validate_Sampler_Config_File +} + +function Ensure_Run_Reproducibility_Sampler() +{ + Copy_Hybrid_Handler_Config_Section 'Sampler' \ + "${HYBRID_software_output_directory[Sampler]}" \ + "$(dirname "$(realpath "${HYBRID_software_executable[Sampler]}")")" +} + +function Run_Software_Sampler() +{ + Separate_Terminal_Output_For 'Sampler' + local -r sampler_config_file_path="${HYBRID_software_configuration_file[Sampler]}" + cd "${HYBRID_software_output_directory[Sampler]}" + "${HYBRID_software_executable[Sampler]}" 'events' '1' \ + "${sampler_config_file_path}" &>> \ + "${HYBRID_software_output_directory[Sampler]}/${HYBRID_terminal_output[Sampler]}" \ + || Report_About_Software_Failure_For 'Sampler' +} + +#=================================================================================================== + +function __static__Validate_Sampler_Config_File() +{ + if ! __static__Is_Sampler_Config_Valid; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + "The sampler configuration file is invalid." + fi +} + +function __static__Transform_Relative_Paths_In_Sampler_Config_File() +{ + local freezeout_path output_directory + freezeout_path=$(__static__Get_Path_Field_From_Sampler_Config_As_Global_Path 'surface') + output_directory=$(__static__Get_Path_Field_From_Sampler_Config_As_Global_Path 'spectra_dir') + Remove_Comments_And_Replace_Provided_Keys_In_Provided_Input_File \ + 'TXT' "${HYBRID_software_configuration_file[Sampler]}" \ + "$(printf "%s: %s\n" \ + 'surface' "${freezeout_path}" \ + 'spectra_dir' "${output_directory}")" +} + +# The following symbolic link is not needed by the sampler, as the sampler only refers to information +# specified in its input file. However, we want to have all input for a software in the output folder +# for future easier reproducibility (and we do so for all software handled in the codebase). +function __static__Create_Superfluous_Symbolic_Link_To_Freezeout_File_Ensuring_Its_Existence() +{ + local freezeout_path + freezeout_path=$(__static__Get_Path_Field_From_Sampler_Config_As_Global_Path 'surface') + Ensure_Input_File_Exists_And_Alert_If_Unfinished "${freezeout_path}" + if [[ "$(dirname "${freezeout_path}")" != "${HYBRID_software_output_directory[Sampler]}" ]]; then + ln -s "${freezeout_path}" \ + "${HYBRID_software_output_directory[Sampler]}/freezeout.dat" + fi +} + +#=================================================================================================== + +function __static__Is_Sampler_Config_Valid() +{ + local -r config_file="${HYBRID_software_configuration_file[Sampler]}" + # Remove empty lines from configuration file + if ! sed -i '/^[[:space:]]*$/d' "${config_file}"; then + Print_Internal_And_Exit "Empty lines removal in ${FUNCNAME} failed." + fi + # Check if the config file is empty + if [[ ! -s "${config_file}" ]]; then + Print_Error 'Sampler configuration file is empty.' + return 1 + fi + # Check for two columns in each line + if [[ $(awk 'NF!=2 {exit 1}' "${config_file}") ]]; then + Print_Error 'Each line should consist of two columns.' + return 1 + fi + # Check that no key is repeated + if [[ $(awk '{print $1}' "${config_file}" | sort | uniq -d) != '' ]]; then + Print_Error 'Found repeated key in sampler configuration file.' + return 1 + fi + # Define allowed keys as an array + local -r allowed_keys=( + 'surface' + 'spectra_dir' + 'number_of_events' + 'rescatter' + 'weakContribution' + 'shear' + 'bulk' + 'ecrit' + 'Nbins' + 'q_max' + 'cs2' + 'ratio_pressure_energydensity' + ) + local keys_to_be_found + keys_to_be_found=2 + while read key value; do + if ! Element_In_Array_Equals_To "${key}" "${allowed_keys[@]}"; then + Print_Error 'Invalid key ' --emph "${key}" ' found in sampler configuration file.' + return 1 + fi + case "${key}" in + surface | spectra_dir) + if [[ "${value}" = '=DEFAULT=' ]]; then + ((keys_to_be_found--)) + continue + fi + ;;& # Continue matching other cases below + surface) + cd "${HYBRID_software_output_directory[Sampler]}" + if [[ ! -f "${value}" ]]; then + cd - > /dev/null + Print_Error 'Freeze-out surface file ' --emph "${value}" ' not found!' + return 1 + fi + ((keys_to_be_found--)) + ;; + spectra_dir) + cd "${HYBRID_software_output_directory[Sampler]}" + if [[ ! -d "${value}" ]]; then + cd - > /dev/null + Print_Error 'Sampler output folder ' --emph "${value}" ' not found!' + return 1 + fi + ((keys_to_be_found--)) + ;; + rescatter | weakContribution | shear | bulk) + if [[ ! "${value}" =~ ^[01]$ ]]; then + Print_Error 'Key ' --emph "${key}" ' must be either ' \ + --emph '0' ' or ' --emph '1' '.' + return 1 + fi + ;; + number_of_events | Nbins) + if [[ ! "${value}" =~ ^[1-9][0-9]*$ ]]; then + Print_Error 'Found not-integer value ' --emph "${value}" \ + ' for ' --emph "${key}" ' key.' + return 1 + fi + ;; + *) + if [[ ! "${value}" =~ ^[+-]?[0-9]+(\.[0-9]*)?$ ]]; then + Print_Error 'Found invalid value ' --emph "${value}" \ + ' for ' --emph "${key}" ' key.' + return 1 + fi + ;; + esac + done < "${config_file}" + # Check that all required keys were found + if [[ ${keys_to_be_found} -gt 0 ]]; then + Print_Error 'Either ' --emph 'surface' ' or ' --emph 'spectra_dir' \ + ' key is missing in sampler configuration file.' + return 1 + fi +} + +function __static__Check_If_Sampler_Configuration_Is_Consistent_With_Hydro() +{ + local -r config_sampler="${HYBRID_software_configuration_file[Sampler]}" + if Element_In_Array_Equals_To 'Hydro' "${HYBRID_given_software_sections[@]}"; then + local -r config_hydro="${HYBRID_software_configuration_file[Hydro]}" + local shear_hydro shear_hydro_param bulk_hydro bulk_hydro_param \ + ecrit_hydro + shear_hydro=0 + shear_hydro_param=0 + bulk_hydro=0 + bulk_hydro_param=0 + ecrit_hydro=0.5 + while read key value; do + case "${key}" in + etaS) + shear_hydro=$(bc -l <<< "${value}>0") + ;; + etaSparam) + shear_hydro_param=$(bc -l <<< "${value}>0") + ;; + zetaS) + bulk_hydro=$(bc -l <<< "${value}>0") + ;; + zetaSparam) + bulk_hydro_param=$(bc -l <<< "${value}>0") + ;; + e_crit) + ecrit_hydro=${value} + ;; + esac + done < "${config_hydro}" + local is_hydro_shear is_hydro_bulk + is_hydro_shear=0 + is_hydro_bulk=0 + if [[ "${shear_hydro}" -eq 1 || "${shear_hydro_param}" -eq 1 ]]; then + is_hydro_shear=1 + fi + if [[ "${bulk_hydro}" -eq 1 || "${bulk_hydro_param}" -eq 1 ]]; then + is_hydro_bulk=1 + fi + local is_sampler_shear is_sampler_bulk \ + ecrit_sampler + is_sampler_shear=1 + is_sampler_bulk=0 + ecrit_sampler=0.5 + while read key value; do + case "${key}" in + shear) + is_sampler_shear=${value} + ;; + bulk) + is_sampler_bulk=${value} + ;; + ecrit) + ecrit_sampler=${value} + ;; + esac + done < "${config_sampler}" + if [[ "${is_hydro_shear}" -eq 1 && "${is_sampler_shear}" -eq 0 ]]; then + __static__State_Inconsistency_Of_Sampler_With_Hydro 'shear' + fi + if [[ "${is_hydro_bulk}" -eq 1 && "${is_sampler_bulk}" -eq 0 ]]; then + __static__State_Inconsistency_Of_Sampler_With_Hydro 'bulk' + fi + if ! awk '{if($1==$2){exit 0}else{exit 1}}' <<< "${ecrit_hydro} ${ecrit_sampler}" &> /dev/null; then + Print_Attention 'The threshold energy density in the sampler (' \ + --emph "${ecrit_sampler}" ')\nis not equal to the threshold energy density in hydrodynamics (' \ + --emph "${ecrit_hydro}" ').\n' \ + --emph 'ecrit' ' in the sampler configuration file is reset to ' --emph "${ecrit_hydro}" '!' + Remove_Comments_And_Replace_Provided_Keys_In_Provided_Input_File \ + 'TXT' "${config_sampler}" \ + "$(printf "%s %s\n" 'ecrit' "${ecrit_hydro}")" + fi + fi +} + +function __static__State_Inconsistency_Of_Sampler_With_Hydro() +{ + PrintAttention 'The sampler and hydrodynamics parameters' \ + 'are inconsistent in values for ' --emph "$1" ' correction.' \ + 'Viscous corrections are present in hydrodynamic stage,' \ + 'but will not be applied in the Cooper-Frye sampling,' \ + 'Please, ensure that this is desired behavior.' +} + +function __static__Get_Path_Field_From_Sampler_Config_As_Global_Path() +{ + local field value + field="$1" + # We assume here that the configuration file is fine as it was validated before + value=$(awk -v name="${field}" '$1 == name {print $2; exit}' \ + "${HYBRID_software_configuration_file[Sampler]}") + if [[ "${value}" = '=DEFAULT=' ]]; then + case "${field}" in + surface) + printf "${HYBRID_software_output_directory[Hydro]}/freezeout.dat" + ;; + spectra_dir) + printf "${HYBRID_software_output_directory[Sampler]}" + ;; + esac + else + cd "${HYBRID_software_output_directory[Sampler]}" || exit ${HYBRID_fatal_builtin} + # If realpath succeeds, it prints the path that is the result of the function + if ! realpath "${value}" 2> /dev/null; then + exit_code=${HYBRID_fatal_file_not_found} Print_Fatal_And_Exit \ + 'Unable to transform relative path ' --emph "${value}" ' into global one.' + fi + cd - > /dev/null || exit ${HYBRID_fatal_builtin} + fi +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/command_line_parsers/allowed_options.bash b/bash/command_line_parsers/allowed_options.bash new file mode 100644 index 0000000..b659e82 --- /dev/null +++ b/bash/command_line_parsers/allowed_options.bash @@ -0,0 +1,31 @@ +#=================================================== +# +# Copyright (c) 2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +# These variable declarations are gathered in a function in order to allow for +# reuse when implementing in-terminal manuals and/or autocompletion. +# +# NOTE: Since this file is sourced by the autocompletion code in the user +# environment, it is important to keep to the minimum any pollution there. +# Therefore this function has a prefix and it starts by '_'. Furthermore +# this function is not marked as readonly to avoid errors in case the user +# sources this file multiple times e.g. through their ~/.bashrc file. +function _HYBRID_Declare_Allowed_Command_Line_Options() +{ + # This associative array is meant to map execution modes to allowed options + # in such a mode. A single associative array instead of many different arrays + # makes it cleaner for the user environment. However, it is then needed to + # assume that no option contains spaces and use word splitting from the caller. + # This is a very reasonable assumption, though. + declare -rgA HYBRID_allowed_command_line_options=( + ['help']='' + ['version']='' + ['do']='--output-directory --configuration-file --id' + ['prepare-scan']='--output-directory --configuration-file --scan-name' + ) +} diff --git a/bash/command_line_parsers/helper.bash b/bash/command_line_parsers/helper.bash new file mode 100644 index 0000000..47face6 --- /dev/null +++ b/bash/command_line_parsers/helper.bash @@ -0,0 +1,178 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Give_Required_Help() +{ + case "${HYBRID_execution_mode}" in + help) + __static__Print_Main_Help_Message + ;; + do-help | prepare-scan-help) + _HYBRID_Declare_Allowed_Command_Line_Options + __static__Print_Help_Message_For_Given_Mode "${HYBRID_execution_mode%%-help}" + ;; + *) + Print_Internal_And_Exit \ + 'Unexpected value of ' --emph "HYBRID_execution_mode=${HYBRID_execution_mode}" \ + ' in ' --emph "${FUNCNAME}" + ;; + esac + +} + +function __static__Print_Main_Help_Message() +{ + # NOTE: This function is thought to be run in ANY user system and it might be + # that handler prerequisites are missing. Hence, possibly only bash should be used. + declare -A section_headers auxiliary_modes_description execution_modes_description + section_headers=( + ['auxiliary_modes_description']='Auxiliary modes for help, information or setup' + ['execution_modes_description']='Prepare needed files and folders and/or submit/run new simulation(s)' + ) + auxiliary_modes_description=( + ['help']='Display this help message' + ['version']='Get information about the version in use' + ) + execution_modes_description=( + ['do']='Do everything is necessary to run the workflow given in the configuration file' + ['prepare-scan']='Prepare configurations files for the handler scanning in the given parameters' + ) + __static__Print_Handler_Header_And_Usage_Synopsis + __static__Print_Modes_Description + Check_System_Requirements_And_Make_Report +} + +function __static__Print_Help_Message_For_Given_Mode() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty HYBRID_allowed_command_line_options + local -r mode="$1" + __static__Print_Help_Header_For_Given_Mode_Help "${mode}" + local option + # Let word splitting split allowed command line options + for option in ${HYBRID_allowed_command_line_options["${mode}"]}; do + __static__Print_Given_Command_Line_Option_Help "${option}" + done +} + +function __static__Print_Handler_Header_And_Usage_Synopsis() +{ + printf '\e[96m%s\e[0m\n' \ + ' #----------------------------------------------------------------------------#' \ + ' # __ __ __ _ __ __ __ ____ #' \ + ' # / / / /_ __/ /_ _____(_)___/ / / / / /___ _____ ____/ / /__ _____ #' \ + ' # / /_/ / / / / __ \/ ___/ / __ / / /_/ / __ `/ __ \/ __ / / _ \/ ___/ #' \ + ' # / __ / /_/ / /_/ / / / / /_/ / / __ / /_/ / / / / /_/ / / __/ / #' \ + ' # /_/ /_/\__, /_.___/_/ /_/\__,_/ /_/ /_/\__,_/_/ /_/\__,_/_/\___/_/ #' \ + ' # /____/ #' \ + ' # #' \ + ' #----------------------------------------------------------------------------#' + printf '\n' + printf '\e[38;5;85m%s\e[0m\n' \ + ' USAGE: Hybrid-handler [--help] [--version]' \ + ' [...]' + printf '\n' +} + +function __static__Print_Modes_Description() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty section_headers "${!section_headers[@]}" + local section mode section_string + printf '\e[38;5;38m %s\e[0m\n' \ + 'Here in the following you find an overview of the existing execution modes.' + for section in "${!section_headers[@]}"; do + printf "\n \e[93m${section_headers[${section}]}\e[0m\n" + declare -n list_of_modes="${section}" + section_string='' + for mode in "${!list_of_modes[@]}"; do + # Remember that $(...) strip trailing '\n' -> Add new-line manually to the string + section_string+="$( + printf \ + '\e[38;5;85m%15s \e[96m%s\e[0m' \ + "${mode}" \ + "${list_of_modes[${mode}]}" + )"$'\n' + done + if hash sort &> /dev/null; then + # Remember that the 'here-string' adds a newline to the string when + # feeding it into the command -> get rid of it here + sort --ignore-leading-blanks <<< "${section_string%?}" + else + printf "%s" "${section_string}" + fi + done + printf '\n\e[38;5;38m %s \e[38;5;85m%s \e[38;5;38m%s\e[0m\n\n' \ + 'Use' '--help' 'after each non auxiliary mode to get further information about it.' +} + +function __static__Print_Help_Header_For_Given_Mode_Help() +{ + printf '\e[38;5;38m %s \e[38;5;85m%s \e[38;5;38m%s\e[0m\n' \ + 'You can specify the following command line options to the' "$1" 'execution mode:' +} + +function __static__Print_Given_Command_Line_Option_Help() +{ + case "$1" in + --output-directory) + __static__Print_Command_Line_Option_Help \ + '-o | --output-directory' "${HYBRID_output_directory/${PWD}/.}" \ + 'Directory where the output folder(s) will be created.' + ;; + --configuration-file) + __static__Print_Command_Line_Option_Help \ + '-c | --configuration-file' "${HYBRID_configuration_file}" \ + 'YAML configuration file to be used by the handler.' + ;; + --scan-name) + __static__Print_Command_Line_Option_Help \ + '-s | --scan-name' "./$(realpath -m --relative-to=. "${HYBRID_scan_directory}")" \ + 'Label of the scan used by the handler to produce output.' \ + 'The new configuration files will be put in a sub-folder' \ + 'of the output directory named using the specified name' \ + 'and the configuration files themselves will contain the' \ + 'scan name as part of their name.' + ;; + --id) + __static__Print_Command_Line_Option_Help \ + '--id' "${HYBRID_run_id}" \ + 'Run ID to be used by the handler. The timestamp in the' \ + 'default value refers to when the hybrid handler is run.' + ;; + *) + Print_Internal_And_Exit \ + 'Unknown option ' --emph "$1" ' passed to ' --emph "${FUNCNAME}" ' function.' + ;; + esac +} + +function __static__Print_Command_Line_Option_Help() +{ + local -r \ + length_option=30 indentation=' ' \ + column_sep=' ' \ + options_color='\e[38;5;85m' \ + text_color='\e[96m' \ + default_value_color='\e[93m' \ + default_text='\e[0m' + local name default_value description left_column left_column_length + name=$1 + default_value=$2 + description=$3 + shift 3 + printf -v left_column "${indentation}${options_color}%*s${column_sep}" ${length_option} "${name}" + left_column_length=$((${#indentation} + ${length_option} + ${#column_sep})) + printf "\n${left_column}${text_color}%s${default_text}\n" "${description}" + while [[ $# -gt 0 ]]; do + printf "%${left_column_length}s${text_color}%s${default_text}\n" '' "$1" + shift + done + printf "%${left_column_length}sDefault: ${default_value_color}${default_value}${default_text}\n" '' +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/command_line_parsers/main_parser.bash b/bash/command_line_parsers/main_parser.bash new file mode 100644 index 0000000..9793e0f --- /dev/null +++ b/bash/command_line_parsers/main_parser.bash @@ -0,0 +1,177 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Parse_Execution_Mode() +{ + if [[ "${#HYBRID_command_line_options_to_parse[@]}" -eq 0 ]]; then + return 0 + fi + __static__Replace_Short_Options_With_Long_Ones + # Locally set function arguments to take advantage of shift + set -- "${HYBRID_command_line_options_to_parse[@]}" + case "$1" in + help | --help) + HYBRID_execution_mode='help' + ;; + version | --version) + HYBRID_execution_mode='version' + ;; + format | --format) + HYBRID_execution_mode='format' + ;; + do) + HYBRID_execution_mode='do' + ;; + prepare-scan) + HYBRID_execution_mode='prepare-scan' + ;; + *) + exit_code=${HYBRID_fatal_command_line} Print_Fatal_And_Exit \ + 'Specified mode ' --emph "$1" ' not valid! Run ' \ + --emph 'Hybrid-handler help' ' to get further information.' + ;; + esac + shift + # Update the global array with remaining options to be parsed + HYBRID_command_line_options_to_parse=("$@") + # Ignore any command line option in some specific cases + if [[ ${HYBRID_execution_mode} =~ ^(help|version)$ ]]; then + HYBRID_command_line_options_to_parse=() + elif Element_In_Array_Equals_To '--help' "${HYBRID_command_line_options_to_parse[@]}"; then + HYBRID_execution_mode+='-help' + HYBRID_command_line_options_to_parse=() + fi + __static__Store_Remaining_Command_Line_Arguments_Into_Global_Associative_Array +} + +# NOTE: The strategy of the following function is to +# 1) validate if command-line options are allowed in given mode; +# 2) parse mode-specific options invoking sub-parser; +# 3) parse remaining options (which are in common to two or more modes) all together. +# Since options are validated, step number 3 simplifies the implementation +# without the risk of accepting invalid options. +function Parse_Command_Line_Options() +{ + if [[ ! ${HYBRID_execution_mode} =~ ^(do|prepare-scan)$ ]]; then + Print_Internal_And_Exit \ + 'Function ' --emph "${FUNCNAME}" ' should not be called in ' \ + --emph "${HYBRID_execution_mode}" ' execution mode.' + fi + __static__Validate_Command_Line_Options + Call_Function_If_Existing_Or_No_Op Parse_Specific_Mode_Options_${HYBRID_execution_mode} + set -- "${HYBRID_command_line_options_to_parse[@]}" + while [[ $# -gt 0 ]]; do + case "$1" in + --output-directory) + if [[ ${2-} =~ ^(-|$) ]]; then + Print_Option_Specification_Error_And_Exit "$1" + else + if ! realpath "$2" &> /dev/null; then + exit_code=${HYBRID_fatal_file_not_found} Print_Fatal_And_Exit \ + 'Specified output directory ' --emph "$2" ' not found' + fi + readonly HYBRID_output_directory="$(realpath "$2")" + readonly HYBRID_scan_directory="${HYBRID_output_directory}/$(basename "${HYBRID_scan_directory}")" + fi + shift 2 + ;; + --configuration-file) + if [[ ${2-} =~ ^(-|$) ]]; then + Print_Option_Specification_Error_And_Exit "$1" + else + readonly HYBRID_configuration_file="$(realpath "$2")" + fi + shift 2 + ;; + --id) + if [[ ${2-} =~ ^(-|$) ]]; then + Print_Option_Specification_Error_And_Exit "$1" + else + HYBRID_run_id=$2 + fi + shift 2 + ;; + *) + exit_code=${HYBRID_fatal_command_line} Print_Fatal_And_Exit \ + 'Invalid option ' --emph "$1" ' specified in ' --emph "${HYBRID_execution_mode}" \ + ' execution mode!' 'Use the ' --emph '--help' ' option to get further information.' + ;; + esac + done +} + +function __static__Replace_Short_Options_With_Long_Ones() +{ + declare -A options_map=( + ['-c']='--configuration-file' + ['-h']='--help' + ['-o']='--output-directory' + ['-s']='--scan-name' + ) + set -- "${HYBRID_command_line_options_to_parse[@]}" + HYBRID_command_line_options_to_parse=() + local option + for option in "$@"; do + if Element_In_Array_Equals_To "${option}" "${!options_map[@]}"; then + option=${options_map["${option}"]} + fi + HYBRID_command_line_options_to_parse+=("${option}") + done +} + +function __static__Validate_Command_Line_Options() +{ + _HYBRID_Declare_Allowed_Command_Line_Options + # Let word splitting split valid options + local -r valid_options=(${HYBRID_allowed_command_line_options["${HYBRID_execution_mode}"]}) + local option + for option in "${HYBRID_command_line_options_to_parse[@]}"; do + if [[ ${option} =~ ^- ]]; then + if ! Element_In_Array_Equals_To "${option}" "${valid_options[@]}"; then + exit_code=${HYBRID_fatal_command_line} Print_Fatal_And_Exit \ + 'Option ' --emph "${option}" ' is not allowed in ' --emph "${HYBRID_execution_mode}" \ + ' execution mode!' 'Use the ' --emph '--help' ' mode option to get further information.' + fi + fi + done +} + +function __static__Store_Remaining_Command_Line_Arguments_Into_Global_Associative_Array() +{ + set -- "${HYBRID_command_line_options_to_parse[@]}" + if [[ ! ${1-} =~ ^(-|$) ]]; then + Print_Internal_And_Exit \ + 'First argument is not an option starting with dash in\n' --emph "${FUNCNAME}" '.' + fi + while [[ $# -gt 0 ]]; do + case "$1" in + -*) + if [[ ! ${2-} =~ ^(-|$) ]]; then + HYBRID_command_line_options_given_to_handler["$1"]="$2" + shift + fi + if [[ ! ${2-} =~ ^(-|$) ]]; then + # This if-clause might become a while-loop and here one could append to the + # array entry with a separator. However, such a separator should not be + # contained in the values and it is not yet necessary to take such a decision. + Print_Internal_And_Exit \ + 'Unexpected if-clause entered in ' --emph "${FUNCNAME}" '.' \ + 'Multiple values for command line options not yet supported.' + fi + shift + ;; + *) + Print_Internal_And_Exit 'Unexpected case entered in ' --emph "${FUNCNAME}" '.' + ;; + esac + done + readonly -A HYBRID_command_line_options_given_to_handler +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/command_line_parsers/sub_parser.bash b/bash/command_line_parsers/sub_parser.bash new file mode 100644 index 0000000..3e1cb0b --- /dev/null +++ b/bash/command_line_parsers/sub_parser.bash @@ -0,0 +1,32 @@ +#=================================================== +# +# Copyright (c) 2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Parse_Specific_Mode_Options_prepare-scan() +{ + set -- "${HYBRID_command_line_options_to_parse[@]}" + HYBRID_command_line_options_to_parse=() + while [[ $# -gt 0 ]]; do + case "$1" in + --scan-name) + if [[ ${2-} =~ ^(-|$) ]]; then + Print_Option_Specification_Error_And_Exit "$1" + else + # This will be set readonly later as parsing '-o' will change it + HYBRID_scan_directory="${HYBRID_output_directory}/$2" + fi + shift 2 + ;; + *) + HYBRID_command_line_options_to_parse+=("$1") + shift + ;; + esac + done + +} diff --git a/bash/common_functionality.bash b/bash/common_functionality.bash new file mode 100644 index 0000000..f523a0b --- /dev/null +++ b/bash/common_functionality.bash @@ -0,0 +1,84 @@ +#=================================================== +# +# Copyright (c) 2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Create_Output_Directory_For() +{ + mkdir -p "${HYBRID_software_output_directory[$1]}" || exit ${HYBRID_fatal_builtin} +} + +function Copy_Base_Configuration_To_Output_Folder_For() +{ + cp "${HYBRID_software_base_config_file[$1]}" \ + "${HYBRID_software_configuration_file[$1]}" || exit ${HYBRID_fatal_builtin} +} + +function Replace_Keys_In_Configuration_File_If_Needed_For() +{ + local file_type + case "$1" in + IC | Afterburner) + file_type='YAML' + ;; + Hydro | Sampler) + file_type='TXT' + ;; + *) + Print_Internal_And_Exit 'Wrong call of ' --emph "${FUNCNAME}" ' function.' + ;; + esac + if [[ "${HYBRID_software_new_input_keys[$1]}" != '' ]]; then + Remove_Comments_And_Replace_Provided_Keys_In_Provided_Input_File \ + "${file_type}" \ + "${HYBRID_software_configuration_file[$1]}" \ + "${HYBRID_software_new_input_keys[$1]}" + fi +} + +function Separate_Terminal_Output_For() +{ + if [[ -e "${HYBRID_software_output_directory[$1]}/${HYBRID_terminal_output[$1]}" ]]; then + local -r \ + timestamp=$(date +'%d-%m-%Y %H:%M:%S') \ + width=80 + { + printf '\n\n\n' + Print_Line_of_Equals ${width} + Print_Centered_Line 'NEW RUN OUTPUT' ${width} '' '=' + Print_Centered_Line "${timestamp}" ${width} '' '=' + Print_Line_of_Equals ${width} + printf '\n\n' + } >> "${HYBRID_software_output_directory[$1]}/${HYBRID_terminal_output[$1]}" + fi +} + +function Report_About_Software_Failure_For() +{ + exit_code=${HYBRID_fatal_software_failed} Print_Fatal_And_Exit \ + '\n' --emph "$1" ' run failed.' +} + +function Ensure_Input_File_Exists_And_Alert_If_Unfinished() +{ + # NOTE: 'input_file' variables follow symbolic links + # 'realpath -m' is used to accept non existing paths + local -r \ + input_file="$(realpath -m "$1")" \ + input_file_unfinished="$(realpath -m "${1}.unfinished")" + if [[ ! -f "${input_file}" ]]; then + if [[ -f "${input_file_unfinished}" ]]; then + Ensure_Given_Files_Exist \ + "Instead of the correct input file, a different file was found:\n - " \ + --emph "${input_file_unfinished}" \ + "\nIt is possible the previous stage of the simulation failed." \ + -- "${input_file}" + else + Ensure_Given_Files_Exist "${input_file}" + fi + fi +} diff --git a/bash/configuration_parser.bash b/bash/configuration_parser.bash new file mode 100644 index 0000000..32d587d --- /dev/null +++ b/bash/configuration_parser.bash @@ -0,0 +1,205 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +# NOTE: We do not want to change the config file, hence we read sections into +# local (string) variables and then use these as input to yq to read keys +# and deleting the key from the variable content. +function Validate_And_Parse_Configuration_File() +{ + Ensure_Given_Files_Exist \ + 'A configuration file is needed to run the hybrid handler.' '--' \ + "${HYBRID_configuration_file}" + __static__Abort_If_Configuration_File_Is_Not_A_Valid_YAML_File + __static__Abort_If_Sections_Are_Violating_Any_Requirement + __static__Abort_If_Invalid_Keys_Were_Used + __static__Parse_Section 'Hybrid_handler' + __static__Parse_Section 'IC' + __static__Parse_Section 'Hydro' + __static__Parse_Section 'Sampler' + __static__Parse_Section 'Afterburner' +} + +function __static__Abort_If_Configuration_File_Is_Not_A_Valid_YAML_File() +{ + if ! yq "${HYBRID_configuration_file}" &> /dev/null; then + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + 'The handler configuration file does not contain valid YAML syntax.' + fi +} + +function __static__Abort_If_Sections_Are_Violating_Any_Requirement() +{ + local input_section_labels label index valid_software_label software_sections_indices + # Here word splitting separates keys into array entries, hence we assume keys do not contain spaces! + input_section_labels=($(yq 'keys | .[]' "${HYBRID_configuration_file}")) + software_sections_indices=() + for label in "${input_section_labels[@]}"; do + if Element_In_Array_Equals_To "${label}" "${HYBRID_valid_auxiliary_configuration_sections[@]}"; then + continue + else + for index in "${!HYBRID_valid_software_configuration_sections[@]}"; do + valid_software_label=${HYBRID_valid_software_configuration_sections[index]} + if [[ "${valid_software_label}" = "${label}" ]]; then + software_sections_indices+=(${index}) + continue 2 + fi + done + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + 'Invalid section ' --emph "${label}" ' found in the handler configuration file.' + fi + done + # Here all given sections are valid. Check possible duplicates/holes and ordering using the stored indices + if [[ ${#software_sections_indices[@]} -eq 0 ]]; then + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + 'No software section was specified in the handler configuration file.' + elif [[ ${#software_sections_indices[@]} -gt 1 ]]; then + local -r number_of_distinct_sections=$(sort -u <(printf '%d\n' "${software_sections_indices[@]}") | wc -l) + if [[ ${number_of_distinct_sections} -ne ${#software_sections_indices[@]} ]]; then + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + 'The same software section in the handler configuration file cannot be repeated.' + fi + if ! sort -C <(printf '%d\n' "${software_sections_indices[@]}"); then + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + 'Software sections in the handler configuration file are out of order.' + fi + local gaps_between_indices + gaps_between_indices=$(awk 'NR>1{print $1-x}{x=$1}' \ + <(printf '%d\n' "${software_sections_indices[@]}") | sort -u) + if [[ "${gaps_between_indices}" != '1' ]]; then + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + 'Missing software section(s) in the handler configuration file.' + fi + fi +} + +function __static__Abort_If_Invalid_Keys_Were_Used() +{ + local -r yaml_config="$(< "${HYBRID_configuration_file}")" + local valid_keys invalid_report + invalid_report=() # Use array entries to split lines to feed into logger + # Hybrid_handler section + valid_keys=("${!HYBRID_hybrid_handler_valid_keys[@]}") + __static__Validate_Keys_Of_Section 'Hybrid_handler' + # IC section + valid_keys=("${!HYBRID_ic_valid_keys[@]}") + __static__Validate_Keys_Of_Section 'IC' + # Hydro section + valid_keys=("${!HYBRID_hydro_valid_keys[@]}") + __static__Validate_Keys_Of_Section 'Hydro' + # Sampler section + valid_keys=("${!HYBRID_sampler_valid_keys[@]}") + __static__Validate_Keys_Of_Section 'Sampler' + # Afterburner section + valid_keys=("${!HYBRID_afterburner_valid_keys[@]}") + __static__Validate_Keys_Of_Section 'Afterburner' + # Abort if some validation failed + if [[ "${#invalid_report[@]}" -ne 0 ]]; then + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + 'Following invalid keys found in the handler configuration file:' \ + '---------------------------------------------------------------' \ + "${invalid_report[@]/%/:}" \ + '---------------------------------------------------------------' + fi +} + +function __static__Validate_Keys_Of_Section() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty yaml_config + Ensure_That_Given_Variables_Are_Set invalid_report valid_keys + local -r section_label=$1 + local invalid_keys yaml_section + if Has_YAML_String_Given_Key "${yaml_config}" "${section_label}"; then + yaml_section="$(Read_From_YAML_String_Given_Key "${yaml_config}" "${section_label}")" + invalid_keys=($(__static__Get_Top_Level_Invalid_Keys_In_Given_YAML_string "${yaml_section}")) + if [[ ${#invalid_keys[@]} -ne 0 ]]; then + invalid_report+=(" ${section_label}" "${invalid_keys[@]/#/ }") + fi + fi +} + +# NOTE: It is assumed that keys do not contain spaces! +function __static__Get_Top_Level_Invalid_Keys_In_Given_YAML_string() +{ + Ensure_That_Given_Variables_Are_Set valid_keys + local input_keys key invalid_keys + input_keys=($(yq 'keys | .[]' <<< "$1")) + invalid_keys=() + for key in "${input_keys[@]}"; do + if ! Element_In_Array_Equals_To "${key}" "${valid_keys[@]}"; then + invalid_keys+=("${key}") + fi + done + printf '%s ' "${invalid_keys[@]}" +} + +function __static__Parse_Section() +{ + local -r \ + section_label=$1 \ + yaml_config="$(< "${HYBRID_configuration_file}")" + local yaml_section valid_key + if Has_YAML_String_Given_Key "${yaml_config}" "${section_label}"; then + yaml_section="$(Read_From_YAML_String_Given_Key "${yaml_config}" "${section_label}")" + if [[ ${section_label} = 'Hybrid_handler' ]]; then + if Has_YAML_String_Given_Key "${yaml_section}" 'LHS_scan' \ + && [[ "${HYBRID_execution_mode}" != 'prepare-scan' ]]; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'The ' --emph 'LHS_scan' ' key can only be specified in the ' \ + --emph 'prepare-scan' ' execution mode!' + fi + else + HYBRID_given_software_sections+=("${section_label}") + fi + declare -n reference_to_map="HYBRID_${section_label,,}_valid_keys" + for valid_key in "${!reference_to_map[@]}"; do + Ensure_That_Given_Variables_Are_Set "${reference_to_map[${valid_key}]}" + __static__Parse_Key_And_Store_It "${valid_key}" "${reference_to_map[${valid_key}]}" + done + __static__YAML_section_must_be_empty "${yaml_section}" "${section_label}" + fi +} + +function __static__Parse_Key_And_Store_It() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty yaml_section + local key=$1 + declare -n store_variable=$2 + if Has_YAML_String_Given_Key "${yaml_section}" "${key}"; then + store_variable=$(Read_From_YAML_String_Given_Key "${yaml_section}" "${key}") + if Element_In_Array_Equals_To "${key}" "${HYBRID_boolean_keys[@]}"; then + __static__Validate_Boolean_Value "${key}" "$2" + fi + yaml_section="$(Print_YAML_String_Without_Given_Key "${yaml_section}" "${key}")" + fi +} + +function __static__Validate_Boolean_Value() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty store_variable + local -r key=$1 + if [[ ! "${store_variable^^}" =~ ^(TRUE|FALSE)$ ]]; then + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + 'Invalid value found at ' --emph "${key}: ${store_variable}" \ + '.' 'The value is expected to be a YAML 1.2 boolean one (e.g. ' \ + --emph 'true|false' ').' + fi + store_variable=${store_variable^^} # Ensure string is fully capitalized +} + +function __static__YAML_section_must_be_empty() +{ + local yaml_section=$1 + if [[ "${yaml_section}" != '{}' ]]; then + Print_Internal_And_Exit \ + 'Not all keys in ' --emph "${2:-some}" ' section have been parsed. Remaining:' \ + --emph "\n${yaml_section}\n" + fi +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/dispatch_functions.bash b/bash/dispatch_functions.bash new file mode 100644 index 0000000..c9ae3a2 --- /dev/null +++ b/bash/dispatch_functions.bash @@ -0,0 +1,34 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Prepare_Software_Input_File() +{ + Print_Info 'Preparing software input file for ' --emph "$1" ' stage' + Call_Function_If_Existing_Or_Exit ${FUNCNAME}_$1 "${@:2}" +} + +function Ensure_All_Needed_Input_Exists() +{ + Print_Info 'Ensuring all needed input for ' --emph "$1" ' stage exists' + Call_Function_If_Existing_Or_Exit ${FUNCNAME}_$1 "${@:2}" +} + +function Ensure_Run_Reproducibility() +{ + Print_Info 'Preparing ' --emph "$1" ' reproducibility metadata' + Call_Function_If_Existing_Or_Exit ${FUNCNAME}_$1 "${@:2}" +} + +function Run_Software() +{ + Print_Info 'Running ' --emph "$1" ' software' + Call_Function_If_Existing_Or_Exit ${FUNCNAME}_$1 "${@:2}" +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/error_codes.bash b/bash/error_codes.bash new file mode 100644 index 0000000..4e2629c --- /dev/null +++ b/bash/error_codes.bash @@ -0,0 +1,26 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +# Standard bash exit codes +readonly HYBRID_success_exit_code=0 +readonly HYBRID_failure_exit_code=1 + +# Variables for exit codes (between 64 and 113) +# -> http://tldp.org/LDP/abs/html/exitcodes.html +readonly HYBRID_fatal_builtin=64 +readonly HYBRID_fatal_file_not_found=65 +readonly HYBRID_fatal_wrong_config_file=66 +readonly HYBRID_fatal_command_line=67 +readonly HYBRID_fatal_value_error=68 +readonly HYBRID_fatal_missing_requirement=69 +readonly HYBRID_fatal_software_failed=70 +readonly HYBRID_fatal_logic_error=110 +readonly HYBRID_fatal_missing_feature=111 +readonly HYBRID_fatal_variable_unset=112 +readonly HYBRID_internal_exit_code=113 diff --git a/bash/execution_mode_do.bash b/bash/execution_mode_do.bash new file mode 100644 index 0000000..0800b8a --- /dev/null +++ b/bash/execution_mode_do.bash @@ -0,0 +1,22 @@ +#=================================================== +# +# Copyright (c) 2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Do_Needed_Operations_For_Given_Software() +{ + printf '\e[1A' # Just aesthetics to 'skip' newline for first loop iteration + local software_section + for software_section in "${HYBRID_given_software_sections[@]}"; do + # Here abuse the logger to simply indent date + Print_Info -l -- "\n$(printf '\e[0m%s' "$(date)")" + Prepare_Software_Input_File "${software_section}" + Ensure_All_Needed_Input_Exists "${software_section}" + Ensure_Run_Reproducibility "${software_section}" + Run_Software "${software_section}" + done +} diff --git a/bash/execution_mode_prepare.bash b/bash/execution_mode_prepare.bash new file mode 100644 index 0000000..3255521 --- /dev/null +++ b/bash/execution_mode_prepare.bash @@ -0,0 +1,29 @@ +#=================================================== +# +# Copyright (c) 2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Do_Needed_Operation_For_Parameter_Scan() +{ + # This array is going to contain a map between the parameter and its list + # of values. Since in general a scan run could request a scan in parameters + # of different stages, here the parameter name is stored as a period-separated + # list of keys as they appear in the Hybrid Handler configuration file, and + # precisely in the 'Software_keys' sections. For example a scan in Hydro 'etaS' + # would be stored here in the component with key 'Hydro.Software_keys.etaS'. + # This syntax is handy when it comes to prepare all the configuration files + # as it naturally interacts well with yq. The value is a YAML sequence of the + # parameter values. + declare -A list_of_parameters_values + Format_Scan_Parameters_Lists + Print_Info 'Validating input scan parameters values' + Validate_And_Store_Scan_Parameters + Print_Info 'Collecting parameters scan values' + Create_List_Of_Parameters_Values + Print_Info 'Preparing scan configuration files:' + Create_And_Populate_Scan_Folder +} diff --git a/bash/formatter.bash b/bash/formatter.bash new file mode 100644 index 0000000..8195c76 --- /dev/null +++ b/bash/formatter.bash @@ -0,0 +1,33 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Format_Codebase() +{ + if ! hash shfmt &> /dev/null; then + Print_Fatal_And_Exit \ + 'Command ' --emph 'shfmt' ' not available, unable to format codebase.' \ + 'Please, install it (https://github.com/mvdan/sh#shfmt) and run the formatting again.' + else + Ensure_That_Given_Variables_Are_Set_And_Not_Empty HYBRID_top_level_path + shfmt -w -ln bash -i 4 -bn -ci -sr -fn "${HYBRID_top_level_path}" + fi +} + +function Run_Formatting_Unit_Test() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty HYBRID_top_level_path + source "${HYBRID_top_level_path}/tests/unit_tests_formatting.bash" || exit ${HYBRID_fatal_builtin} + # The following variable definition is just a patch to be able to reuse the test code from here + HYBRIDT_repository_top_level_path="${HYBRID_top_level_path}" + if Unit_Test__codebase-formatting; then + Print_Info 'The codebase was correctly formatted and the formatting test passes.' + fi +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/global_variables.bash b/bash/global_variables.bash new file mode 100644 index 0000000..50a2775 --- /dev/null +++ b/bash/global_variables.bash @@ -0,0 +1,182 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +# ATTENTION: The top-level section labels (i.e. 'IC', 'Hydro', etc.) are used in variable +# names as well, although with lower-case letters only. In the codebase it has +# been exerted leverage on this aspect and at some point the name of variables +# are built using the section labels transformed into lower-case words. Hence, +# it is important that section labels do not contain characters that would break +# this mechanism, like dashes or spaces! +function Define_Further_Global_Variables() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty HYBRID_top_level_path + # Constant information + readonly HYBRID_valid_software_configuration_sections=( + 'IC' + 'Hydro' + 'Sampler' + 'Afterburner' + ) + readonly HYBRID_valid_auxiliary_configuration_sections=( + 'Hybrid_handler' + ) + readonly HYBRID_default_configurations_folder="${HYBRID_top_level_path}/configs" + readonly HYBRID_python_folder="${HYBRID_top_level_path}/python" + readonly HYBRID_afterburner_list_filename="sampled_particles_list.oscar" + readonly HYBRID_default_number_of_samples=0 + declare -rgA HYBRID_external_python_scripts=( + [Add_spectators_from_IC]="${HYBRID_python_folder}/add_spectators.py" + [Latin_hypercube_sampling]="${HYBRID_python_folder}/latin_hypercube_sampling.py" + ) + declare -rgA HYBRID_software_default_input_filename=( + [IC]='' + [Hydro]="SMASH_IC.dat" + [Sampler]="freezeout.dat" # Not used at the moment for how the sampler works + [Spectators]="SMASH_IC.oscar" + [Afterburner]="particle_lists.oscar" + ) + declare -rgA HYBRID_software_configuration_filename=( + [IC]='IC_config.yaml' + [Hydro]='hydro_config.txt' + [Sampler]='sampler_config.txt' + [Afterburner]='afterburner_config.yaml' + ) + declare -rgA HYBRID_terminal_output=( + [IC]='IC.log' + [Hydro]='Hydro.log' + [Sampler]='Sampler.log' + [Afterburner]='Afterburner.log' + ) + declare -rgA HYBRID_handler_config_section_filename=( + [IC]='Hybrid_handler_IC_config.yaml' + [Hydro]='Hybrid_handler_Hydro_config.yaml' + [Sampler]='Hybrid_handler_Sampler_config.yaml' + [Afterburner]='Hybrid_handler_Afterburner_config.yaml' + ) + # This array specifies a set of YAML lists of keys that define a valid parameter scan. + # ATTENTION: The keys inside each YAML list must be alphabetically sorted to allow + # the validation mechanism to work! + readonly HYBRID_valid_scan_specification_keys=( + '[Range]' + '[Values]' + ) + readonly HYBRID_scan_combinations_filename='scan_combinations.dat' + # The following associative arrays declare maps between valid keys in the handler config + # file and bash variables in which the input information will be stored once parsed. + declare -rgA HYBRID_hybrid_handler_valid_keys=( + [Run_ID]='HYBRID_run_id' + [LHS_scan]='HYBRID_number_of_samples' + ) + declare -rgA HYBRID_ic_valid_keys=( + [Executable]='HYBRID_software_executable[IC]' + [Config_file]='HYBRID_software_base_config_file[IC]' + [Scan_parameters]='HYBRID_scan_parameters[IC]' + [Software_keys]='HYBRID_software_new_input_keys[IC]' + ) + declare -rgA HYBRID_hydro_valid_keys=( + [Executable]='HYBRID_software_executable[Hydro]' + [Config_file]='HYBRID_software_base_config_file[Hydro]' + [Input_file]='HYBRID_software_user_custom_input_file[Hydro]' + [Scan_parameters]='HYBRID_scan_parameters[Hydro]' + [Software_keys]='HYBRID_software_new_input_keys[Hydro]' + ) + declare -rgA HYBRID_sampler_valid_keys=( + [Executable]='HYBRID_software_executable[Sampler]' + [Config_file]='HYBRID_software_base_config_file[Sampler]' + [Scan_parameters]='HYBRID_scan_parameters[Sampler]' + [Software_keys]='HYBRID_software_new_input_keys[Sampler]' + ) + declare -rgA HYBRID_afterburner_valid_keys=( + [Executable]='HYBRID_software_executable[Afterburner]' + [Config_file]='HYBRID_software_base_config_file[Afterburner]' + [Input_file]='HYBRID_software_user_custom_input_file[Afterburner]' + [Scan_parameters]='HYBRID_scan_parameters[Afterburner]' + [Software_keys]='HYBRID_software_new_input_keys[Afterburner]' + [Add_spectators_from_IC]='HYBRID_optional_feature[Add_spectators_from_IC]' + [Spectators_source]='HYBRID_optional_feature[Spectators_source]' + ) + # This array declares a list of boolean keys. Here we do not keep track of sections + # as it would be strange to use the same key name in different sections once as + # boolean and once as something else. + declare -rg HYBRID_boolean_keys=( + 'Add_spectators_from_IC' + ) + # This array will be filled by the parser as option-to-value(s) map and it is intended + # to track information given on the command line. For information like the run ID that + # can be given both on the command line and in the configuration file, the latter comes + # after and it naturally has precedence. Thanks to this array we can "give precedence" + # and use the input from the command line. + declare -gA HYBRID_command_line_options_given_to_handler=() + # Variables to be set (and possibly made readonly) from command line + HYBRID_execution_mode='help' + HYBRID_configuration_file='./config.yaml' + HYBRID_output_directory="$(realpath './data')" + HYBRID_scan_directory="${HYBRID_output_directory}/scan" + # Variables which can be specified both from command line and from configuration/setup + HYBRID_run_id="Run_$(date +'%Y-%m-%d_%H%M%S')" + # Variables to be set (and possibly made readonly) from configuration/setup + HYBRID_number_of_samples="${HYBRID_default_number_of_samples}" + HYBRID_given_software_sections=() + declare -gA HYBRID_software_executable=( + [IC]='' + [Hydro]='' + [Sampler]='' + [Afterburner]='' + ) + declare -gA HYBRID_software_user_custom_input_file=( + [IC]='' + [Hydro]='' + [Sampler]='' + [Spectators]='' + [Afterburner]='' + ) + declare -gA HYBRID_software_base_config_file=( + [IC]="${HYBRID_default_configurations_folder}/smash_initial_conditions.yaml" + [Hydro]="${HYBRID_default_configurations_folder}/vhlle_hydro" + [Sampler]="${HYBRID_default_configurations_folder}/hadron_sampler" + [Afterburner]="${HYBRID_default_configurations_folder}/smash_afterburner.yaml" + ) + declare -gA HYBRID_scan_parameters=( + [IC]='' + [Hydro]='' + [Sampler]='' + [Afterburner]='' + ) + declare -gA HYBRID_software_new_input_keys=( + [IC]='' + [Hydro]='' + [Sampler]='' + [Afterburner]='' + ) + declare -gA HYBRID_optional_feature=( + [Add_spectators_from_IC]='TRUE' + [Spectators_source]='' + ) + # Variables to be set (and possibly made readonly) after all sanity checks on input succeeded + declare -gA HYBRID_software_output_directory=( + [IC]='' + [Hydro]='' + [Sampler]='' + [Afterburner]='' + ) + declare -gA HYBRID_software_configuration_file=( + [IC]='' + [Hydro]='' + [Sampler]='' + [Afterburner]='' + ) + declare -gA HYBRID_software_input_file=( + [Hydro]='' + [Spectators]='' + [Afterburner]='' + ) + HYBRID_scan_strategy='Combinations' +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/logger.bash b/bash/logger.bash new file mode 100644 index 0000000..8e94db2 --- /dev/null +++ b/bash/logger.bash @@ -0,0 +1,431 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== +# +# This file has been taken from the BashLogger project (v0.2) +# https://github.com/AxelKrypton/BashLogger +# and, as requested, its original license header is reported here below. +# +#---------------------------------------------------------------------------------------- +# +# Copyright (c) 2019,2023-2024 +# Alessandro Sciarra +# +# This file is part of BashLogger. +# +# BashLogger is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BashLogger is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BashLogger. If not, see . +# +#---------------------------------------------------------------------------------------- +# +# The logger will print output to the chosen file descriptor (by default 42). This is +# done (instead of simply let it print to standard output) to be able to use the logger +# in functions that "return" by printing to stdout and that are meant to be called in $(). +# +# ATTENTION: It might be checked if the chosen fd exists and in case open it in the Logger +# function itself. However, if the first Logger call is done in a subshell, then +# the chosen fd would not be open globally for the script and following calls to +# the Logger would fail. Hence, we open the fd at source time and not close it +# in the Logger. +# +# NOTE: Nothing is done if this file is executed and not sourced! +# +if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + BSHLGGR_outputFd=42 + BSHLGGR_defaultExitCode=1 + while [[ $# -gt 0 ]]; do + case "$1" in + --fd) + if [[ ! $2 =~ ^[1-9][0-9]*$ ]] || (($2 > 254)); then + printf "Error sourcing BashLogger. '$1' option needs an integer value between 1 and 254.\n" + return 1 + else + BSHLGGR_outputFd=$2 + shift 2 + fi + ;; + --default-exit-code) + if [[ ! $2 =~ ^[0-9]+$ ]] || (($2 > 255)); then + printf "Error sourcing BashLogger. '$1' option needs an integer value between 0 and 255.\n" + return 1 + else + BSHLGGR_defaultExitCode=$2 + shift 2 + fi + ;; + *) + printf "Error sourcing BashLogger. Unknown option '$1'.\n" + return 1 + ;; + esac + done + # Probably redundant check, but we want guarantee that 'eval' is safe to use here + # e.g. that the BSHLGGR_outputFd cannot be set to '; rm -rf /; #' (AAARGH!). + if [[ ! ${BSHLGGR_outputFd} =~ ^[1-9][0-9]*$ ]]; then + printf "Unexpected error sourcing BashLogger. Please contact developers.\n" + return 1 + else + eval "exec ${BSHLGGR_outputFd}>&1" + fi + readonly BSHLGGR_outputFd BSHLGGR_defaultExitCode +fi + +function PrintTrace() +{ + __static__Logger 'TRACE' "$@" +} +function Print_Trace() +{ + PrintTrace "$@" +} + +function PrintDebug() +{ + __static__Logger 'DEBUG' "$@" +} +function Print_Debug() +{ + PrintDebug "$@" +} + +function PrintInfo() +{ + __static__Logger 'INFO' "$@" +} +function Print_Info() +{ + PrintInfo "$@" +} + +function PrintAttention() +{ + __static__Logger 'ATTENTION' "$@" +} +function Print_Attention() +{ + PrintAttention "$@" +} + +function PrintWarning() +{ + __static__Logger 'WARNING' "$@" +} +function Print_Warning() +{ + PrintWarning "$@" +} + +function PrintError() +{ + __static__Logger 'ERROR' "$@" +} +function Print_Error() +{ + PrintError "$@" +} + +function PrintFatalAndExit() +{ + __static__Logger 'FATAL' "$@" +} +function Print_Fatal_And_Exit() +{ + PrintFatalAndExit "$@" +} + +function PrintInternalAndExit() +{ + __static__Logger 'INTERNAL' "$@" +} +function Print_Internal_And_Exit() +{ + PrintInternalAndExit "$@" +} + +function __static__Logger() +{ + if [[ $# -lt 1 ]]; then + __static__Logger 'INTERNAL' "${FUNCNAME} called without label!" + elif [[ $# -lt 2 ]]; then + set -- "$1" '' # Set empty message if none provided + fi + local label labelLength labelToBePrinted color emphColor finalEndline restoreDefault + finalEndline='\n' + restoreDefault='\e[0m' + labelLength=10 + label="$1" + shift + labelToBePrinted=$(printf "%${labelLength}s" "${label}:") + if [[ ! ${label} =~ ^(INTERNAL|FATAL|ERROR|WARNING|ATTENTION|INFO|DEBUG|TRACE)$ ]]; then + __static__Logger 'INTERNAL' "${FUNCNAME} called with unknown label '${label}'!" + fi + __static__IsLevelOn "${label}" || return 0 + exec 4>&1 # duplicate fd 1 to restore it later + case "${label}" in + ERROR | FATAL) + # ;;& means go on in case matching following patterns + color='\e[91m' + ;;& + INTERNAL) + color='\e[38;5;202m' + ;;& + ERROR | FATAL | INTERNAL) + emphColor='\e[93m' + exec 1>&2 # here stdout to stderr! + ;; + INFO) + color='\e[92m' + emphColor='\e[96m' + ;;& + ATTENTION) + color='\e[38;5;200m' + emphColor='\e[38;5;141m' + ;;& + WARNING) + color='\e[93m' + emphColor='\e[38;5;202m' + ;;& + DEBUG) + color='\e[38;5;38m' + emphColor='\e[38;5;48m' + ;;& + TRACE) + color='\e[38;5;247m' + emphColor='\e[38;5;256m' + ;;& + *) + exec 1>&"${BSHLGGR_outputFd}" # here stdout to chosen fd + ;; + esac + if __static__IsElementInArray '--' "$@"; then + while [[ "$1" != '--' ]]; do + case "$1" in + -n) + finalEndline='' + shift + ;; + -l) + labelToBePrinted="$(printf "%${labelLength}s" '')" + shift + ;; + -d) + restoreDefault='' + shift + ;; + *) + __static__Logger 'INTERNAL' "${FUNCNAME} called with unknown option \"$1\"!" + ;; + esac + done + shift + fi + # Print out initial new-lines before label suppressing first argument if it was endlines only + # Note that endlines could be either '\n' or $'\n' and we want to deal with both. + local new_first_argument + while [[ $1 =~ ^\\n || "${1:0:1}" = $'\n' ]]; do + printf '\n' + if [[ $1 =~ ^\\n ]]; then + new_first_argument="${1/#\\n/}" + elif [[ "${1:0:1}" = $'\n' ]]; then + new_first_argument="${1/#$'\n'/}" + fi + if [[ "${new_first_argument}" = '' ]]; then + if [[ $# -eq 1 ]]; then + return + else + shift + fi + else + set -- "${new_first_argument}" "${@:2}" + fi + done + # Ensure something to print was given + if [[ $# -eq 0 ]]; then + __static__Logger 'INTERNAL' "${FUNCNAME} called without message (or with new lines only)!" + fi + # Parse all arguments and save messages for later, possibly modified + local messagesToBePrinted emphNextString lastStringWasEmph indentation message + local -r const_indentation="${labelToBePrinted//?/ } " + messagesToBePrinted=() + emphNextString='FALSE' + lastStringWasEmph='FALSE' + indentation='' + while [[ $# -gt 0 ]]; do + case "$1" in + --emph) + # Here two cases should be handled: either '--emph' is an option or a string + # -> it is literal if the last command line option or if following the option + # -> it is an option otherwise + # Use a fallthrough to continue matching in the case construct and use *) case + # to print. However, shift and continue must be given if '--emph' was an option. + if [[ $# -eq 1 || ${emphNextString} = 'TRUE' ]]; then + : + else + emphNextString='TRUE' + shift + continue + fi + ;;& + *) + if [[ ${#messagesToBePrinted[@]} -gt 0 ]]; then + indentation="${labelToBePrinted//?/ } " + fi + # Set color and replace % by %% for later printf + if [[ ${emphNextString} = 'TRUE' ]]; then + message=$(__static__ReplaceEndlinesInMessage "$1") + messagesToBePrinted+=("${emphColor}${message//%/%%}") + lastStringWasEmph='TRUE' + else + if [[ ${lastStringWasEmph} = 'FALSE' ]]; then + if [[ ${#messagesToBePrinted[@]} -gt 0 ]]; then + messagesToBePrinted[-1]+='\n' + fi + else + indentation='' + fi + message=$(__static__ReplaceEndlinesInMessage "$1") + messagesToBePrinted+=("${indentation}${color}${message//%/%%}") + lastStringWasEmph='FALSE' + fi + emphNextString='FALSE' + ;; + esac + shift + done + # Last message has no endline, add 'finalEndline' to it + messagesToBePrinted[-1]+="${finalEndline}" + set -- "${messagesToBePrinted[@]}" + # Print first line + printf "\e[1m${color}${labelToBePrinted}\e[0m $1" + shift + # Print possible additional lines + while [[ $# -gt 0 ]]; do + printf "$1" + shift + done + if [[ ${label} = 'INTERNAL' ]]; then + printf "${labelToBePrinted//?/ } Please, contact developers.\n" + fi + printf "${restoreDefault}" + exec 1>&4- # restore fd 1 and close fd 4 and not close chosen fd (it must stay open, see top of the file!) + if [[ ${label} =~ ^(FATAL|INTERNAL)$ ]]; then + exit "${exit_code:-${BSHLGGR_defaultExitCode}}" + fi +} + +function __static__ReplaceEndlinesInMessage() +{ + # Go through the message to be added character by character and prepend indentation + # to literal '\n' -> Do this with pure bash not to rely on external tools. Using the + # BASH_REMATCH array would make it tougher to iterate "left to right" hitting the replaced + # patterns only once in a general fashion. + # + # NOTE: It is assumed here that the caller has defined the 'const_indentation' variable. + local index oneChar twoChars + message='' + for ((index = 0; index < ${#1}; index++)); do + oneChar="${1:${index}:1}" # one chatacter from index + twoChars="${1:${index}:2}" # two chatacters from index + if [[ "${twoChars}" = '\n' ]]; then + if [[ ${index} -eq 0 || "${1:$((index - 1)):1}" != '\' ]]; then + message+="\n${const_indentation}" + ((index++)) || true # In case logger is used with errexit enabled + continue + fi + elif [[ "${oneChar}" = $'\n' ]]; then + message+="\n${const_indentation}" + continue + fi + message+="${oneChar}" + done + printf '%s' "${message}" +} + +function __static__IsLevelOn() +{ + local label + label="$1" + # FATAL and INTERNAL always on + if [[ ${label} =~ ^(FATAL|INTERNAL)$ ]]; then + return 0 + fi + # VERBOSE environment variable defines how verbose the output should be: + # - unset, empty, invalid value -> till INFO (no DEBUG TRACE) + # - numeric -> till that level (1=ERROR, 2=WARNING, ...) + # - string -> till that level + local loggerLevels loggerLevelsOn level index + loggerLevels=([1]='ERROR' [2]='WARNING' [3]='ATTENTION' [4]='INFO' [5]='DEBUG' [6]='TRACE') + loggerLevelsOn=() + if [[ ${VERBOSE-} =~ ^[0-9]+$ ]]; then + loggerLevelsOn=("${loggerLevels[@]:1:VERBOSE}") + elif [[ ${VERBOSE-} =~ ^(ERROR|WARNING|ATTENTION|INFO|DEBUG|TRACE)$ ]]; then + for level in "${loggerLevels[@]}"; do + loggerLevelsOn+=("${level}") + if [[ ${VERBOSE-} = "${level}" ]]; then + break + fi + done + elif [[ ${VERBOSE-} =~ ^(FATAL|INTERNAL)$ ]]; then + loggerLevelsOn=('FATAL') + else + loggerLevelsOn=('FATAL' 'ERROR' 'WARNING' 'ATTENTION' 'INFO') + fi + for level in "${loggerLevelsOn[@]}"; do + if [[ ${label} = "${level}" ]]; then + return 0 + fi + done + return 1 +} + +function __static__IsElementInArray() +{ + # ATTENTION: Since this function is used in the middle of the logger, the + # logger cannot be used in this function otherwise fd 4 is closed! + local elementToBeFound arrayEntry + elementToBeFound="$1" + for arrayEntry in "${@:2}"; do + if [[ "${arrayEntry}" = "${elementToBeFound}" ]]; then + return 0 + fi + done + return 1 +} + +#-----------------------------------------------------------------# +#Set functions readonly to avoid possible redefinitions elsewhere +readonly -f \ + PrintTrace \ + PrintDebug \ + PrintInfo \ + PrintAttention \ + PrintWarning \ + PrintError \ + PrintFatalAndExit \ + PrintInternalAndExit \ + Print_Trace \ + Print_Debug \ + Print_Info \ + Print_Attention \ + Print_Warning \ + Print_Error \ + Print_Fatal_And_Exit \ + Print_Internal_And_Exit \ + __static__Logger \ + __static__IsLevelOn \ + __static__IsElementInArray diff --git a/bash/progress_bar.bash b/bash/progress_bar.bash new file mode 100644 index 0000000..094d235 --- /dev/null +++ b/bash/progress_bar.bash @@ -0,0 +1,83 @@ +#=================================================== +# +# Copyright (c) 2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Print_Progress_Bar +{ + __static__Validate_Progress_Bar_Input "$@" + Print_Info -l -- "\e[K$(__static__Get_Progress_Bar "$@")\r\e[1A" +} + +function Print_Final_Progress_Bar +{ + __static__Validate_Progress_Bar_Input "$@" + Print_Info -l -- "\e[K$(__static__Get_Progress_Bar "$@")" +} + +function __static__Validate_Progress_Bar_Input() +{ + if [[ $# -lt 2 ]]; then + Print_Internal_And_Exit \ + --emph "${FUNCNAME[1]}" ' wrongly called: ' --emph 'Missing arguments' '.' + elif [[ ! $1 =~ ^[0-9]+(.[0-9]+)*$ ]] || [[ ! $2 =~ ^[0-9]+(.[0-9]+)*$ ]]; then + Print_Internal_And_Exit \ + --emph "${FUNCNAME[1]}" ' wrongly called: ' --emph 'wrong first two arguments' '.' + elif [[ ${5-} != '' ]]; then + if [[ $5 -gt 100 ]] || [[ $5 -lt 1 ]]; then + Print_Internal_And_Exit \ + --emph "${FUNCNAME[1]}" ' wrongly called: ' --emph "$5 not in [0,100]" '.' + fi + fi +} + +function __static__Get_Progress_Bar() +{ + local -r \ + done=$1 \ + total=$2 \ + prefix="${3-}" \ + suffix="${4-}" \ + width_percentage="${5:-33}" \ + bar="━" \ + half_bar_right="╸" + local output="${prefix} " color + local -r \ + width=$((COLUMNS * width_percentage / 100)) \ + percentage=$(awk '{printf "%.0f", 100*$1/$2}' <<< "${done} ${total}") + if [[ ${width} -lt 1 ]]; then + Print_Internal_And_Exit 'Progress bar too short: ' --emph "width = ${width}" + fi + if [[ ${percentage} -lt 30 ]]; then + color='\e[91m' + elif [[ ${percentage} -lt 40 ]]; then + color='\e[38;5;202m' + elif [[ ${percentage} -lt 50 ]]; then + color='\e[38;5;208m' + elif [[ ${percentage} -lt 60 ]]; then + color='\e[38;5;221m' + elif [[ ${percentage} -lt 70 ]]; then + color='\e[38;5;191m' + elif [[ ${percentage} -lt 80 ]]; then + color='\e[38;5;148m' + elif [[ ${percentage} -lt 90 ]]; then + color='\e[38;5;118m' + else + color='\e[92m' + fi + output+=$(printf "${color}") + local -r width_done=$((width * percentage / 100)) + for ((i = 0; i < ${width_done}; i++)); do + output+="${bar}" + done + output+="${half_bar_right}\e[38;5;237m" + for ((i = ${width_done}; i < width; i++)); do + output+="${bar}" + done + output+=$(printf '\e[96m %3d%%\e[0m %s' ${percentage} "${suffix}") + printf '%s' "${output}" +} diff --git a/bash/sanity_checks.bash b/bash/sanity_checks.bash new file mode 100644 index 0000000..d3c4459 --- /dev/null +++ b/bash/sanity_checks.bash @@ -0,0 +1,219 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Perform_Sanity_Checks_On_Provided_Input_And_Define_Auxiliary_Global_Variables() +{ + __static__Perform_Command_Line_VS_Configuration_Consistency_Checks + # Note that the fine-grained Python requirement check has to be done after the type + # of parameter scan has been set, which is done done in the first function here. + __static__Perform_Logic_Checks_Depending_On_Execution_Mode + __static__Exit_If_Some_Further_Needed_Python_Requirement_Is_Missing + local key + for key in "${HYBRID_valid_software_configuration_sections[@]}"; do + # The software output directories are always ALL set, even if not all software is run. This + # is important as some software might rely on files in directories of other workflow blocks. + HYBRID_software_output_directory[${key}]="${HYBRID_output_directory}/${key}/${HYBRID_run_id}" + if Element_In_Array_Equals_To "${key}" "${HYBRID_given_software_sections[@]}"; then + __static__Ensure_Executable_Exists "${key}" + __static__Set_Software_Configuration_File "${key}" + __static__Set_Software_Input_Data_File_If_Not_Set_By_User "${key}" + fi + done + __static__Set_Software_Input_Data_File_If_Not_Set_By_User 'Spectators' + __static__Set_Global_Variables_As_Readonly +} + +# This "static" function is put here and not below "non static" ones as it should be often updated +function __static__Set_Global_Variables_As_Readonly() +{ + readonly \ + HYBRID_software_output_directory \ + HYBRID_software_configuration_file \ + HYBRID_software_input_file \ + HYBRID_software_executable \ + HYBRID_software_user_custom_input_file \ + HYBRID_software_base_config_file \ + HYBRID_software_new_input_keys \ + HYBRID_optional_feature +} + +function Perform_Internal_Sanity_Checks() +{ + Internally_Ensure_Given_Files_Exist \ + 'These Python scripts should be shipped within the hybrid handler codebase.' '--' \ + "${HYBRID_external_python_scripts[@]}" + Internally_Ensure_Given_Files_Exist \ + 'These base configuration files should be shipped within the hybrid handler codebase.' '--' \ + "${HYBRID_software_base_config_file[@]}" +} + +#=================================================================================================== + +function __static__Perform_Command_Line_VS_Configuration_Consistency_Checks() +{ + Internally_Ensure_Given_Files_Exist "${HYBRID_configuration_file}" + if Has_YAML_String_Given_Key "$(< "${HYBRID_configuration_file}")" 'Hybrid_handler.Run_ID' \ + && Element_In_Array_Equals_To '--id' "${!HYBRID_command_line_options_given_to_handler[@]}"; then + Print_Attention 'The run ID was specified both in the configuration file and as command line option.' + Print_Warning -l -- 'The value specified as ' --emph 'command line option' ' will be used!\n' + HYBRID_run_id="${HYBRID_command_line_options_given_to_handler['--id']}" + fi + readonly HYBRID_run_id +} + +function __static__Perform_Logic_Checks_Depending_On_Execution_Mode() +{ + case "${HYBRID_execution_mode}" in + do) + local key + for key in "${!HYBRID_scan_parameters[@]}"; do + if [[ "${HYBRID_scan_parameters["${key}"]}" != '' ]]; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'Configuration key ' --emph 'Scan_parameters' ' can ONLY be specified in ' \ + --emph 'parameter-scan' ' execution mode.' + fi + done + ;; + prepare-scan) + if [[ "${HYBRID_number_of_samples}" -eq ${HYBRID_default_number_of_samples} ]]; then + readonly HYBRID_scan_strategy='Combinations' + elif [[ ! "${HYBRID_number_of_samples}" =~ ^[1-9][0-9]*$ ]] \ + || [[ "${HYBRID_number_of_samples}" -eq 1 ]]; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'The number of samples for Latin Hypercube Sampling scan ' \ + 'has to be ' --emph 'an integer greater than 1' '.' + else + readonly HYBRID_scan_strategy='LHS' + fi + readonly HYBRID_number_of_samples + ;; + help) ;; # This is the default mode which is set in tests -> do nothing, but catch it + *) + Print_Internal_And_Exit 'Unknown execution mode passed to ' --emph "${FUNCNAME}" ' function.' + ;; + esac +} + +function __static__Exit_If_Some_Further_Needed_Python_Requirement_Is_Missing() +{ + if ! Is_Python_Requirement_Satisfied 'packaging' &> /dev/null; then + return # In this case we cannot do anything and the user should have already be warned + fi + Ensure_That_Given_Variables_Are_Set_And_Not_Empty HYBRID_python_requirements + local requirement unsatisfied_requirements=() + for requirement in "${!HYBRID_python_requirements[@]}"; do + case "${requirement}" in + pyDOE*) + if [[ ${HYBRID_execution_mode} = 'prepare-scan' ]] && [[ ${HYBRID_scan_strategy} = 'LHS' ]]; then + if ! Is_Python_Requirement_Satisfied "${requirement}" &> /dev/null; then + unsatisfied_requirements+=("${requirement}") + fi + fi + ;; + PyYAML*) + if [[ ${HYBRID_execution_mode} = 'do' ]] \ + && Element_In_Array_Equals_To 'Afterburner' "${HYBRID_given_software_sections[@]}" \ + && [[ ${HYBRID_optional_feature[Add_spectators_from_IC]} = 'TRUE' ]]; then + if ! Is_Python_Requirement_Satisfied "${requirement}" &> /dev/null; then + unsatisfied_requirements+=("${requirement}") + fi + fi + ;; + *) ;; + esac + done + if [[ ${#unsatisfied_requirements[@]} -gt 0 ]]; then + exit_code=${HYBRID_fatal_missing_requirement} Print_Fatal_And_Exit \ + 'The following Python requirements are not satisfied but needed for this run:\n' \ + --emph "$(printf ' %s\n' "${unsatisfied_requirements[@]}")" \ + '' '\nUnable to continue.' + fi +} + +function __static__Ensure_Executable_Exists() +{ + local label=$1 executable + executable="${HYBRID_software_executable[${label}]}" + if [[ "${executable}" = '' ]]; then + exit_code=${HYBRID_fatal_variable_unset} Print_Fatal_And_Exit \ + 'Software executable for ' --emph "${label}" ' run was not specified.' + elif [[ "${executable}" = / ]]; then + if [[ ! -f "${executable}" ]]; then + exit_code=${HYBRID_fatal_file_not_found} Print_Fatal_And_Exit \ + 'The executable file for the ' --emph "${label}" ' run was not found.' \ + 'Not existing path: ' --emph "${file_path}" + elif [[ ! -x "${executable}" ]]; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'The executable file for the ' --emph "${label}" ' run is not executable.' \ + 'File path: ' --emph "${file_path}" + fi + # It is important to perform this check with 'type' and not with 'hash' because 'hash' with + # paths always succeed -> https://stackoverflow.com/a/42362142/14967071 + # This will be entered if the user gives something stupid as '~' as executable. + elif ! type -P "${executable}" &> /dev/null; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'The command ' --emph "${executable}" ' specified for the ' \ + --emph "${label}" ' run was not located by the shell.' \ + 'Please check your ' --emph 'PATH' ' environment variable and make sure' \ + 'that ' --emph "type -P \"${executable}\"" ' succeeds in your terminal.' + fi +} + +function __static__Set_Software_Configuration_File() +{ + local label=$1 + printf -v HYBRID_software_configuration_file[${label}] \ + "${HYBRID_software_output_directory[${label}]}/${HYBRID_software_configuration_filename[${label}]}" +} + +function __static__Set_Software_Input_Data_File_If_Not_Set_By_User() +{ + local key=$1 + if [[ ${key} =~ ^(Hydro|Afterburner)$ ]]; then + local filename relative_key + filename="${HYBRID_software_user_custom_input_file[${key}]}" + case "${key}" in + Hydro) + relative_key='IC' + ;; + Afterburner) + relative_key='Sampler' + ;; + esac + if [[ "${filename}" = '' ]]; then + printf -v filename '%s/%s' \ + "${HYBRID_software_output_directory[${relative_key}]}" \ + "${HYBRID_software_default_input_filename[${key}]}" + else + if Element_In_Array_Equals_To "${relative_key}" "${HYBRID_given_software_sections[@]}"; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'Requesting custom ' --emph "${key}" ' input file although executing ' \ + --emph "${relative_key}" ' with default output name.' + fi + fi + HYBRID_software_input_file[${key}]="${filename}" + elif [[ "${key}" = 'Spectators' ]]; then + if [[ "${HYBRID_optional_feature[Add_spectators_from_IC]}" = 'TRUE' ]]; then + if [[ "${HYBRID_optional_feature[Spectators_source]}" != '' ]]; then + HYBRID_software_input_file['Spectators']="${HYBRID_optional_feature[Spectators_source]}" + if Element_In_Array_Equals_To "IC" "${HYBRID_given_software_sections[@]}"; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'Requesting custom ' --emph 'Spectators' ' input file although executing ' \ + --emph 'IC' ' with default output name.' + fi + else + printf -v HYBRID_software_input_file[Spectators] '%s/%s' \ + "${HYBRID_software_output_directory[IC]}" \ + "${HYBRID_software_default_input_filename[Spectators]}" + fi + fi + fi +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/scan_files_operations.bash b/bash/scan_files_operations.bash new file mode 100644 index 0000000..17eb0c9 --- /dev/null +++ b/bash/scan_files_operations.bash @@ -0,0 +1,227 @@ +#=================================================== +# +# Copyright (c) 2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +# NOTE: The parameters names and values are stored in 'list_of_parameters_values' +# which is an associative array. No key order is guaranteed, but it is +# important for reproducibility across different machines to fix some +# ordering. Here we create two normal arrays to sort names and have values +# in the same order. +# +# ATTENTION: Here we use process substitution together with readarray to store data +# into arrays. As getting the exit code of process substitution can be +# tricky (https://unix.stackexchange.com/q/128560/370049) we adopt here +# a workaround which consists in storing the data at first in a variable +# using a standard command substitution. This will exit on error and +# no further error handling is needed. +function Create_And_Populate_Scan_Folder() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty list_of_parameters_values + local -r \ + parameters=("${!list_of_parameters_values[@]}") \ + scan_combinations_file="${HYBRID_scan_directory}/${HYBRID_scan_combinations_filename}" + local auxiliary_string parameters_names parameters_values parameters_combinations + auxiliary_string=$(__static__Get_Fixed_Order_Parameters) + readarray -t parameters_names < <(printf '%s'"${auxiliary_string}") + auxiliary_string=$(__static__Get_Fixed_Order_Parameters_Values) + readarray -t parameters_values < <(printf '%s' "${auxiliary_string}") + auxiliary_string=$(__static__Get_Parameters_Combinations_For_New_Configuration_Files "${parameters_values[@]}") + readarray -t parameters_combinations < <(printf '%s' "${auxiliary_string}") + readonly parameters_names parameters_values parameters_combinations + __static__Validate_And_Create_Scan_Folder + __static__Create_Combinations_File_With_Metadata_Header_Block + __static__Create_Output_Files_In_Scan_Folder_And_Complete_Combinations_File +} + +function __static__Get_Fixed_Order_Parameters() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty list_of_parameters_values + # Sort parameters according to stage: IC, Hydro, Sampler, Afterburner (then alphabetically) + # + # NOTE: Using 'grep' would fail and make the function exit if no match was found + # and therefore it is simply easier to loop over parameters here. + local key parameter + for key in 'IC' 'Hydro' 'Sampler' 'Afterburner'; do + for parameter in "${!list_of_parameters_values[@]}"; do + if [[ ${parameter} = ${key}* ]]; then + printf "${parameter}\n" + fi + done | sort --ignore-case + done +} + +function __static__Get_Fixed_Order_Parameters_Values() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty list_of_parameters_values parameters_names + local name + for name in "${parameters_names[@]}"; do + printf '%s\n' "${list_of_parameters_values["${name}"]}" + done +} + +function __static__Get_Parameters_Combinations_For_New_Configuration_Files() +{ + case "${HYBRID_scan_strategy}" in + 'LHS') + __static__Get_Samples_for_LHS "$@" + ;; + 'Combinations') + __static__Get_All_Parameters_Combinations "$@" + ;; + *) + Print_Internal_And_Exit \ + 'Unknown scan strategy in ' --emph "${FUNCNAME}" ' function.' + ;; + esac +} + +function __static__Get_All_Parameters_Combinations() +{ + local values string_to_be_expanded + for values in "$@"; do + # Get rid of spaces and square brackets and prepare brace expansion. If there + # is only one value, do not add braces as no expansion has to happen. + if [[ "${values}" = *,* ]]; then + string_to_be_expanded+="{${values//[][ ]/}}_" + else + string_to_be_expanded+="${values//[][ ]/}_" + fi + done + # NOTE: The following use of 'eval' is fine since the string that is expanded + # is guaranteed to be validated to contain only YAML int, bool or float. + eval printf '%s\\\n' "${string_to_be_expanded%?}" | sed 's/_/ /g' +} + +# For Latin Hypercube Sampling, a python script creates for each parameter a list of +# values. Each single sample is then created by taking one value from each list in order. +function __static__Get_Samples_for_LHS() +{ + local -r series_of_lists=$( + IFS=',' + printf '%s' "$*" + ) + python3 -c " +import numpy as np +data = np.array([${series_of_lists}]) +for i in range(${HYBRID_number_of_samples}): + print(*data.transpose()[i]) +" +} + +function __static__Validate_And_Create_Scan_Folder() +{ + Ensure_Given_Folders_Do_Not_Exist \ + 'The scan output folder is meant to be freshly created by the handler.' '--' \ + "${HYBRID_scan_directory}" + mkdir -p "${HYBRID_scan_directory}" +} + +function __static__Create_Combinations_File_With_Metadata_Header_Block() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty parameters_names scan_combinations_file + Internally_Ensure_Given_Files_Do_Not_Exist "${scan_combinations_file}" + { + for index in "${!parameters_names[@]}"; do + printf '# Parameter_%d: %s\n' $((index + 1)) "${parameters_names[index]}" + done + printf '#\n#___Run' + for index in "${!parameters_names[@]}"; do + printf ' %20s' "Parameter_$((index + 1))" + done + printf '\n' + } > "${scan_combinations_file}" +} + +function __static__Create_Output_Files_In_Scan_Folder_And_Complete_Combinations_File() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty parameters_combinations + local -r number_of_files=${#parameters_combinations[@]} + local id filename + Print_Progress_Bar 0 ${number_of_files} + for id in "${!parameters_combinations[@]}"; do + filename="$(__static__Get_Output_Filename "${id}")" + # Let word splitting split values in each parameters combination + __static__Add_Line_To_Combinations_File "${id}" ${parameters_combinations[id]} + __static__Create_Single_Output_File_In_Scan_Folder "${id}" ${parameters_combinations[id]} + Print_Progress_Bar \ + $((id + 1)) ${number_of_files} '' "$(printf '%5d' $((id + 1)))/${number_of_files} files" + done + Print_Final_Progress_Bar \ + $((id + 1)) ${number_of_files} '' "$(printf '%5d' $((id + 1)))/${number_of_files} files" +} + +function __static__Get_Output_Filename() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty parameters_combinations + # Here the filename has to contain the run number prefixed with the correct + # amount of leading zeroes in order to make sorting easier for the user. + local -r \ + number_of_combinations=${#parameters_combinations[@]} \ + run_number=$(($1 + 1)) + printf '%s_run_%0*d.yaml' \ + "${HYBRID_scan_directory}/$(basename "${HYBRID_scan_directory}")" \ + "${#number_of_combinations}" \ + "${run_number}" +} + +function __static__Add_Line_To_Combinations_File() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty scan_combinations_file + # These fields lengths are hard-coded for the moment and are meant to + # properly align the column content to the header line description + { + printf '%7d' $(($1 + 1)) + shift + printf ' %20s' "$@" + printf '\n' + } >> "${scan_combinations_file}" +} + +function __static__Create_Single_Output_File_In_Scan_Folder() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty parameters_names filename + local -r run_number=$(($1 + 1)) + shift + local -r set_of_values=("$@") + Internally_Ensure_Given_Files_Do_Not_Exist "${filename}" + __static__Add_Parameters_Comment_Line_To_New_Configuration_File + __static__Add_YAML_Configuration_To_New_Configuration_File + __static__Remove_Scan_Parameters_Key_From_New_Configuration_File +} + +function __static__Add_Parameters_Comment_Line_To_New_Configuration_File() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty \ + parameters_names filename run_number set_of_values + local -r longest_parameter_length=$(wc -L < <(printf '%s\n' "${parameters_names[@]}")) + { + printf '# Run %d of parameter scan "%s"\n#\n' "${run_number}" "${HYBRID_scan_directory}" + local index + for index in "${!parameters_names[@]}"; do + printf '# %*s: %s\n' "${longest_parameter_length}" "${parameters_names[index]}" "${set_of_values[index]}" + done + printf '\n' + } > "${filename}" +} + +function __static__Add_YAML_Configuration_To_New_Configuration_File() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty parameters_names filename set_of_values + local index yq_replacements + for index in ${!parameters_names[@]}; do + yq_replacements+="( .${parameters_names[index]} ) = ${set_of_values[index]} |" + done + yq "${yq_replacements%?}" "${HYBRID_configuration_file}" >> "${filename}" +} + +function __static__Remove_Scan_Parameters_Key_From_New_Configuration_File() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty filename + yq -i 'del(.Hybrid_handler.LHS_scan) | del(.Hybrid_handler | select(length==0))' "${filename}" + yq -i 'del(.. | select(has("Scan_parameters")).Scan_parameters)' "${filename}" +} diff --git a/bash/scan_validation.bash b/bash/scan_validation.bash new file mode 100644 index 0000000..b776b83 --- /dev/null +++ b/bash/scan_validation.bash @@ -0,0 +1,202 @@ +#=================================================== +# +# Copyright (c) 2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +# Since it is not know here how the user has specified the list of scan parameters +# in the YAML configuration, we enforce here the "flow" yq-style, i.e. '[...]' +# -> https://mikefarah.gitbook.io/yq/operators/style#set-flow-quote-style +function Format_Scan_Parameters_Lists() +{ + local key + for key in "${!HYBRID_scan_parameters[@]}"; do + HYBRID_scan_parameters["${key}"]=$(yq '.. style="flow"' <<< "${HYBRID_scan_parameters["${key}"]}") + done + readonly HYBRID_scan_parameters +} + +function Validate_And_Store_Scan_Parameters() +{ + Ensure_That_Given_Variables_Are_Set list_of_parameters_values + # Manually set the array in order to check at the end if it stayed empty. + # NOTE: In 'set -u' mode this is needed, otherwise accessing the size would fail for empty arrays. + list_of_parameters_values=() + local key parameters parameter counter=0 + for key in "${!HYBRID_scan_parameters[@]}"; do + if [[ "${HYBRID_scan_parameters["${key}"]}" = '' ]]; then + continue + else + # Here the --unwrapScalar option of yq is meant to strip quotes and colors to properly + # store the content in a bash array. The yq input is assumed to be a sequence, which + # is basically true by construction, see 'Format_Scan_Parameters_Lists' function. + readarray -t parameters < <(yq --unwrapScalar '.[]' <<< "${HYBRID_scan_parameters["${key}"]}") + for parameter in "${parameters[@]}"; do + if ! __static__Is_Parameter_To_Be_Scanned \ + "${parameter}" "${HYBRID_software_new_input_keys["${key}"]}"; then + ((counter++)) || true + else + parameter_value=$( + Read_From_YAML_String_Given_Key \ + "${HYBRID_software_new_input_keys["${key}"]}" "${parameter}" + ) + # Get rid of YAML top-level 'Scan' key + list_of_parameters_values["${key}.Software_keys.${parameter}"]=$( + yq '.Scan' <<< "${parameter_value}" + ) + fi + done + fi + done + if [[ ${counter} -ne 0 ]]; then + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + '\nThe hybrid handler configuration file contains ' \ + --emph "${counter}" ' invalid scan specifications.' + elif [[ ${#list_of_parameters_values[@]} -eq 0 ]]; then + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + 'The hybrid handler configuration file does not properly specify scan parameters.' \ + 'Make sure to specify the ' --emph 'Scan_parameters' ' key in the appropriate sections.' + fi +} + +function __static__Is_Parameter_To_Be_Scanned() +{ + local -r key="$1" yaml_section="$2" + if ! Has_YAML_String_Given_Key "${yaml_section}" "${key}"; then + Print_Error \ + 'The ' --emph "${key}" ' parameter is asked to be scanned but its value' \ + 'was not specified in the corresponding ' --emph 'Software_keys' '.' + return 1 + fi + local parameter_value + parameter_value=$(Read_From_YAML_String_Given_Key "${yaml_section}" "${key}") + if ! __static__Is_Given_Key_Value_A_Valid_Scan "${parameter_value}"; then + Print_Error -l -- 'The ' --emph "${key}" \ + ' parameter is asked to be scanned but its value was not properly specified as a scan.' + return 1 + fi +} + +function __static__Is_Given_Key_Value_A_Valid_Scan() +{ + local -r given_scan="$1" + __static__Check_If_Given_Scan_Is_A_YAML_Map || return 1 + __static__Check_If_Given_Scan_Is_A_YAML_Map_With_Scan_Key_Only || return 1 + # Now the scan keys can be safely extracted. Note that here we need to take into + # account that the Scan keys might be given by the user in an arbitrary order + # and therefore we need to sort them before comparison + local -r sorted_scan_keys=$(__static__Get_Sorted_Scan_Keys) + __static__Check_If_Given_Scan_Keys_Are_Allowed || return 1 + __static__Check_If_Keys_Of_Given_Scan_Have_Correct_Values || return 1 +} + +function __static__Check_If_Given_Scan_Is_A_YAML_Map() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty given_scan + if [[ $(yq '. | tag' <<< "${given_scan}") != '!!map' ]]; then + Print_Error 'The given scan\n' --emph "${given_scan}" '\nis not a YAML map.' + return 1 + fi +} + +function __static__Check_If_Given_Scan_Is_A_YAML_Map_With_Scan_Key_Only() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty given_scan + if [[ $(yq '. | keys | .. style="flow"' <<< "${given_scan}") != '[Scan]' ]]; then + Print_Error \ + 'The given scan\n' --emph "${given_scan}" '\nis not a YAML map containing only the ' \ + --emph 'Scan' ' key at top level.' + return 1 + fi +} + +function __static__Get_Sorted_Scan_Keys() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty given_scan + yq '.Scan | keys | sort | .. style="flow"' <<< "${given_scan}" +} + +function __static__Check_If_Given_Scan_Keys_Are_Allowed() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty given_scan sorted_scan_keys + if ! Element_In_Array_Equals_To "${sorted_scan_keys}" "${HYBRID_valid_scan_specification_keys[@]}"; then + Print_Error \ + 'The value\n' --emph "${sorted_scan_keys}" '\ndoes not define a valid scan.' \ + 'Refer to the documentation to see which are valid scan specifications.' + return 1 + fi +} + +function __static__Check_If_Keys_Of_Given_Scan_Have_Correct_Values() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty given_scan sorted_scan_keys + if ! __static__Has_Valid_Scan_Correct_Values; then + Print_Error -l -- 'The given scan\n' --emph "${given_scan}" '\nis allowed but its specification is invalid.' + return 1 + fi +} + +function __static__Has_Valid_Scan_Correct_Values() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty given_scan sorted_scan_keys + case "${sorted_scan_keys}" in + "[Values]") + __static__Has_YAML_Key_A_Numeric_Sequence_As_Value 'Values' || return 1 + ;; + "[Range]") + __static__Has_YAML_Key_A_Numeric_Sequence_As_Value 'Range' || return 1 + local range num_values + range=$(yq '.Scan.Range | .. style="flow"' <<< "${given_scan}") + num_values=$(yq '. | length' <<< "${range}") + if ((num_values != 2)); then + Print_Error \ + 'Exactly two values are expected for the range. ' \ + 'The given range contains ' --emph "${num_values}" ' values.' + return 1 + fi + if [[ $(yq '.[0] > .[1]' <<< "${range}") == "true" ]]; then + Print_Error \ + 'The first value must be smaller than the second value in the Range. ' \ + 'The given range is ' --emph "${range}" '.' + return 1 + fi + ;; + *) + Print_Internal_And_Exit \ + 'Unknown scan passed to ' --emph "${FUNCNAME}" ' function.' + ;; + esac +} + +function __static__Has_YAML_Key_A_Numeric_Sequence_As_Value() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty given_scan + local -r key=$1 + if [[ $(yq ".Scan.${key} | tag" <<< "${given_scan}") != '!!seq' ]]; then + Print_Error \ + 'The value ' --emph "$(yq ".Scan.${key}" <<< "${given_scan}")" \ + ' of the ' --emph "${key}" ' key is not a range of parameter values.' + return 1 + fi + local list_of_value_types num_values first_value second_value + list_of_value_types=($(yq ".Scan.${key}[] | tag" <<< "${given_scan}" | sort -u)) + # If there is more than a type it is in general an error, unless there are exactly two, + # which are 'int' and 'float' (this is accepted). Note that the test is done on the + # concatenation of the values against 'float int' as the types are guaranteed to be sorted. + if [[ ${#list_of_value_types[@]} -ne 1 ]]; then + if [[ ${#list_of_value_types[@]} -ne 2 || "${list_of_value_types[*]//!!/}" != 'float int' ]]; then + Print_Error \ + 'The parameter values have different YAML types: ' \ + --emph "${list_of_value_types[*]//!!/}" '.' + return 1 + fi + elif [[ ! ${list_of_value_types[0]} =~ ^!!(bool|int|float)$ ]]; then + Print_Error \ + 'Parameter scans with values of ' --emph "${list_of_value_types[0]//!!/}" \ + ' type are not allowed.' 'Valid parameter types are ' --emph 'bool int float' ', only.' + return 1 + fi +} diff --git a/bash/scan_values_operations.bash b/bash/scan_values_operations.bash new file mode 100644 index 0000000..9f16574 --- /dev/null +++ b/bash/scan_values_operations.bash @@ -0,0 +1,86 @@ +#=================================================== +# +# Copyright (c) 2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +# NOTE: Here it is assumed that the 'list_of_parameters_values' is containing the +# full period-separated list of YAML keys as array keys and the "scan object" +# as values. This function will replace the array values with plain lists of +# parameter values. +function Create_List_Of_Parameters_Values() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty list_of_parameters_values + local parameter + case "${HYBRID_scan_strategy}" in + 'LHS') + declare -A list_of_parameters_ranges + for parameter in "${!list_of_parameters_values[@]}"; do + __static__Generate_And_Store_Parameter_Ranges "${parameter}" + done + # Here the python script which generates the parameters values is assumed to print + # lists of values in the form: 'parameter.name=[x1,x2,x3,...]' and this is parsed + # back into the 'list_of_parameters_values' array in a while-read construct. + local key value + while IFS='=' read -r key value; do + if Element_In_Array_Equals_To "${key}" "${!list_of_parameters_values[@]}"; then + list_of_parameters_values["${key}"]="${value}" + else + Print_Internal_And_Exit \ + 'Processing of parameters failed in ' --emph "${FUNCNAME}" ' function.' + fi + done < <(${HYBRID_external_python_scripts[Latin_hypercube_sampling]} \ + --parameter_names "${!list_of_parameters_ranges[@]}" \ + --parameter_ranges "${list_of_parameters_ranges[@]}" \ + --num_samples ${HYBRID_number_of_samples}) + ;; + 'Combinations') + for parameter in "${!list_of_parameters_values[@]}"; do + __static__Generate_And_Store_Parameter_List_Of_Values "${parameter}" + done + ;; + *) + Print_Internal_And_Exit \ + 'Unknown scan strategy in ' --emph "${FUNCNAME}" ' function.' + ;; + esac +} + +function __static__Generate_And_Store_Parameter_List_Of_Values() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty list_of_parameters_values + __static__Extract_Parameter_Attribute_And_Store_It 'Values' "$1" 'list_of_parameters_values' +} + +function __static__Generate_And_Store_Parameter_Ranges() +{ + Ensure_That_Given_Variables_Are_Set list_of_parameters_ranges + __static__Extract_Parameter_Attribute_And_Store_It 'Range' "$1" 'list_of_parameters_ranges' +} + +function __static__Extract_Parameter_Attribute_And_Store_It() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty list_of_parameters_values + local -r \ + entity=$1 \ + parameter=$2 \ + scan_map="${list_of_parameters_values[$2]}" + declare -n reference_to_storage=$3 + local sorted_scan_keys + sorted_scan_keys="$(yq '. | keys | sort | .. style="flow"' <<< "${scan_map}")" + readonly sorted_scan_keys + case "${sorted_scan_keys}" in + "[Range]" | "[Values]") + reference_to_storage["${parameter}"]=$( + yq '.'"${entity}"' | .. style="flow"' <<< "${scan_map}" + ) + ;; + *) + Print_Internal_And_Exit \ + 'Unknown ' --emph "${sorted_scan_keys}" 'scan in ' --emph "${FUNCNAME[1]}" ' function.' + ;; + esac +} diff --git a/bash/software_input_functionality.bash b/bash/software_input_functionality.bash new file mode 100644 index 0000000..e819212 --- /dev/null +++ b/bash/software_input_functionality.bash @@ -0,0 +1,102 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +# NOTE: It is assumed that '#' is the character to begins comments in input files +function Remove_Comments_And_Replace_Provided_Keys_In_Provided_Input_File() +{ + local style base_input_file keys_to_be_replaced + style=$1 + base_input_file=$2 + keys_to_be_replaced=$3 + Remove_Comments_In_File "${base_input_file}" # this checks for existence, too + case "${style}" in + YAML) + __static__Replace_Keys_Into_YAML_File + ;; + TXT) + __static__Replace_Keys_Into_Txt_File + ;; + *) + Print_Internal_And_Exit "Wrong first argument passed to \"${FUNCNAME}\"." + ;; + esac +} + +function __static__Replace_Keys_Into_YAML_File() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty base_input_file keys_to_be_replaced + # Use yq -P to bring all YAML to same format (crucial for later check on number of lines) + if ! yq -P --inplace "${base_input_file}" 2> /dev/null; then + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + 'File ' --emph "${base_input_file}" ' does not seem to contain valid YAML syntax. Run' \ + --emph " yq -P --inplace \"${base_input_file}\"" \ + "\nto have more information about the problem." + elif ! keys_to_be_replaced=$(yq -P <(printf "${keys_to_be_replaced}\n") 2> /dev/null); then + exit_code=${HYBRID_fatal_value_error} Print_Fatal_And_Exit \ + 'Keys to be replaced do not seem to contain valid YAML syntax.' + fi + local initial_number_of_lines + initial_number_of_lines=$(wc -l < "${base_input_file}") + # Use yq to merge the two "files" into the first one + yq --inplace eval-all '. as $item ireduce ({}; . * $item)' \ + "${base_input_file}" \ + <(printf "${keys_to_be_replaced}\n") + # The merge must not have changed the number of lines of input file. If it did, + # it means that some key was not present and has been appended => Error! + if [[ $(wc -l < "${base_input_file}") -ne ${initial_number_of_lines} ]]; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'One or more provided software input keys were not found to be replaced' \ + 'in the ' --emph "${base_input_file}" ' file.' 'Please, check your configuration file.' + fi +} + +# NOTE: In this function we might try to keep the empty lines, but the YAML library will +# strip them anyway and therefore it is convenient to strip them immediately. +function __static__Replace_Keys_Into_Txt_File() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty base_input_file keys_to_be_replaced + # Strip lines with space only both in file and in the new keys list + sed -i '/^[[:space:]]*$/d' "${base_input_file}" + keys_to_be_replaced="$(sed '/^[[:space:]]*$/d' <(printf "${keys_to_be_replaced}\n"))" + # Impose that both file and new keys have two entries per line + if ! awk 'NF!=2 {exit 1}' "${base_input_file}"; then + exit_code=${HYBRID_fatal_wrong_config_file} Print_Fatal_And_Exit \ + 'File ' --emph "${base_input_file}" ' does not seem to contain two columns per line only!' + elif ! awk 'NF!=2 {exit 1}' <(printf "${keys_to_be_replaced}\n"); then + exit_code=${HYBRID_fatal_value_error} Print_Fatal_And_Exit \ + 'Keys to be replaced do not seem to contain valid key-value syntax.' + fi + if ! awk '$1 ~ /:$/ {exit 1}' "${base_input_file}"; then + exit_code=${HYBRID_fatal_value_error} Print_Fatal_And_Exit \ + 'File ' --emph "${base_input_file}" ' should not have a colon at the end of keys!' + fi + local number_of_fields_per_line + number_of_fields_per_line=( + $(awk 'BEGIN{FS=":"}{print NF}' <(printf "${keys_to_be_replaced}\n") | sort -u) + ) + if [[ ${#number_of_fields_per_line[@]} -gt 1 ]]; then + exit_code=${HYBRID_fatal_value_error} Print_Fatal_And_Exit \ + 'Keys to be replaced do not have consistent colon-terminated keys syntax.' + elif [[ ${number_of_fields_per_line[0]} -gt 2 ]]; then + exit_code=${HYBRID_fatal_value_error} Print_Fatal_And_Exit \ + 'Keys to be replaced seem to use more than a colon after key(s).' + fi + # NOTE: Since the YAML implementation is very general, here we can take advantage of it + # on constraint of inserting (if needed) and then removing a ':' after the "key". + awk -i inplace 'BEGIN{OFS=": "}{print $1, $2}' "${base_input_file}" + if [[ ${number_of_fields_per_line[0]} -eq 1 ]]; then + keys_to_be_replaced=$(awk 'BEGIN{OFS=": "}{print $1, $2}' <<< "${keys_to_be_replaced}") + fi + __static__Replace_Keys_Into_YAML_File + # NOTE: Using ':' as field separator, spaces after it will be preserved, hence there + # is no worry about having potentially the two fields merged into one in printf. + awk -i inplace 'BEGIN{FS=":"}{printf("%-20s%s\n", $1, $2)}' "${base_input_file}" +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/software_operations.bash b/bash/software_operations.bash new file mode 100644 index 0000000..492ccfc --- /dev/null +++ b/bash/software_operations.bash @@ -0,0 +1,60 @@ +#=================================================== +# +# Copyright (c) 2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function Copy_Hybrid_Handler_Config_Section() +{ + local -r \ + section=$1 \ + output_file="$2/${HYBRID_handler_config_section_filename[$1]}" \ + executable_folder=$3 + if [[ -f "${output_file}" ]]; then + exit_code=${HYBRID_fatal_logic_error} Print_Fatal_And_Exit \ + 'The config copy ' --emph "${output_file}" ' already exists.' + fi + local executable_metadata handler_metadata try_extract_section + # NOTE: Using command substitution $(...) directly in printf arguments invalidates the error on + # exit behavior and let the script continue even if functions have non-zero exit code. + # Therefore we temporary store the resulting string in local variables. + executable_metadata=$(__static__Get_Repository_State "${executable_folder}") + handler_metadata=$(__static__Get_Repository_State "${HYBRID_top_level_path}") + try_extract_section=$(__static__Extract_Sections_From_Configuration_File "${section}") + printf '%s\n\n' \ + "# Git describe of executable folder: ${executable_metadata}" \ + "# Git describe of handler folder: ${handler_metadata}" \ + "${try_extract_section}" > "${output_file}" +} + +function __static__Extract_Sections_From_Configuration_File() +{ + local section code=0 + section=$( + yq " + with_entries(select(.key | test(\"(Hybrid_handler|${1})\"))) + | .Hybrid_handler.Run_ID = \"${HYBRID_run_id}\" + " "${HYBRID_configuration_file}" + ) || code=$? + if [[ ${code} -ne 0 ]]; then + Print_Internal_And_Exit 'Failure extracting sections from configuration file for reproducibility.' + else + printf '%s' "${section}" + fi +} + +function __static__Get_Repository_State() +{ + local git_call code=0 + git_call=$(git -C "${1}" describe --long --always --all 2> /dev/null) || code=$? + if [[ ${code} -ne 0 ]]; then + printf 'Not a Git repository' + else + printf "%s" ${git_call} + fi +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/source_codebase_files.bash b/bash/source_codebase_files.bash new file mode 100644 index 0000000..be6ddc6 --- /dev/null +++ b/bash/source_codebase_files.bash @@ -0,0 +1,56 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +function __static__Source_Codebase_Files() +{ + local list_of_files file_to_be_sourced + # Source error codes and fail with hard-coded generic error + source "${HYBRID_top_level_path}/bash/error_codes.bash" || exit 1 + # Source logger using fd 9 (not too small and still smaller than 10 as bash manual suggests) + source "${HYBRID_top_level_path}/bash/logger.bash" \ + --fd 9 --default-exit-code ${HYBRID_internal_exit_code} || exit ${HYBRID_fatal_builtin} + # Source utility functions to be able to mark all functions as readonly + # when sourcing the codebase files (otherwise this would not work) + source "${HYBRID_top_level_path}/bash/utility_functions.bash" || exit ${HYBRID_fatal_builtin} + list_of_files=( + 'Afterburner_functionality.bash' + 'command_line_parsers/allowed_options.bash' + 'command_line_parsers/helper.bash' + 'command_line_parsers/main_parser.bash' + 'command_line_parsers/sub_parser.bash' + 'common_functionality.bash' + 'configuration_parser.bash' + 'dispatch_functions.bash' + 'execution_mode_do.bash' + 'execution_mode_prepare.bash' + 'formatter.bash' + 'global_variables.bash' + 'Hydro_functionality.bash' + 'IC_functionality.bash' + 'progress_bar.bash' + 'Sampler_functionality.bash' + 'sanity_checks.bash' + 'scan_files_operations.bash' + 'scan_validation.bash' + 'scan_values_operations.bash' + 'software_input_functionality.bash' + 'software_operations.bash' + 'system_requirements.bash' + 'version.bash' + ) + for file_to_be_sourced in "${list_of_files[@]}"; do + source "${HYBRID_top_level_path}/bash/${file_to_be_sourced}" || exit ${HYBRID_fatal_builtin} + done +} + +# Call the function above and source the codebase files when this script is sourced +if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + __static__Source_Codebase_Files + Make_Functions_Defined_In_This_File_Readonly +fi diff --git a/bash/system_requirements.bash b/bash/system_requirements.bash new file mode 100644 index 0000000..4d40c92 --- /dev/null +++ b/bash/system_requirements.bash @@ -0,0 +1,673 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +# ATTENTION: The code in this file might look here and there a bit "strange" and not +# what one would write at first. However, this is due to the fact that +# we want to check the availability of many system requirements without +# using them, otherwise the output for those users missing features would +# be more confusing than helpful. Please, keep this in mind if you are +# going to modify this file and/or change requirements. + +function __static__Declare_System_Requirements() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty HYBRID_top_level_path + if ! declare -p HYBRID_version_regex &> /dev/null; then + readonly HYBRID_version_regex='[0-9](.[0-9]+)*' + readonly HYBRID_python_requirements_file="${HYBRID_top_level_path}/python/requirements.txt" + readonly HYBRID_python_test_requirement_tool="${HYBRID_top_level_path}/python/test_requirement.py" + declare -rgA HYBRID_versions_requirements=( + [awk]='4.1' + [bash]='4.4' + [git]='1.8.5' + [sed]='4.2.1' + [tput]='5.7' + [yq]='4.24.2' + [python3]='3.0.0' + ) + declare -rga HYBRID_programs_just_required=( + cat + column + cut + grep + head + realpath + tail + ) + declare -rga HYBRID_gnu_programs_required=(awk sed sort wc) + declare -rga HYBRID_env_variables_required=(TERM) + declare -gA HYBRID_python_requirements + __static__Parse_Python_Requirements_Into_Global_Array + readonly -A HYBRID_python_requirements + fi +} + +function Check_System_Requirements() +{ + __static__Declare_System_Requirements + # NOTE: The following associative array will be used to store system information + # and since bash does not support arrays entries in associative arrays, then + # just strings with information will be used. Each entry of this array + # contains whether the command was found, its version and whether the version + # meets the requirement or not. A '|' is used to separate the fields in the + # string and '---' is used to indicate a negative outcome in the field. + # The same array is used to store the availability of programs that are just + # required, without any version requirement. For these, only the found field + # is stored (either 'found' or '---'). + # The same array is used to store the availability of GNU tools. For these, + # the key is prefixed by 'GNU-' and the content has a first "field" that is + # 'found' or '---' and a second one that is either 'OK' or '---' to indicate + # whether the GNU version of the command is in use or not. + # The same array is used for environment variables and, in this case, + # the content is either 'OK' or '---'. + # Finally, the same array is used for Python requirements. The keys are the + # Python requirements and the values have one field only which is either + # 'OK' or '---' or '?' or 'wrong'. + # + # ATTENTION: The code relies on the "fields" in system_information elements not + # containing spaces. This assumption might be dropped, but that array + # is an implementation detail and we have full control on its content. + declare -A system_information + __static__Analyze_System_Properties + __static__Exit_If_Some_GNU_Requirement_Is_Missing + __static__Exit_If_Minimum_Versions_Are_Not_Available + __static__Exit_If_Some_Needed_Environment_Variable_Is_Missing + __static__Exit_If_Some_Always_Needed_Python_Requirement_Is_Missing +} + +function Check_System_Requirements_And_Make_Report() +{ + __static__Declare_System_Requirements + local system_report=() + local -r single_field_length=18 # This variable is used to prepare the report correctly formatted + declare -A system_information # Same use of this variable as in 'Check_System_Requirements' function + # Define colors for all reports + local -r \ + emph_color='\e[96m' \ + red='\e[91m' \ + green='\e[92m' \ + yellow='\e[93m' \ + text_color='\e[38;5;38m' \ + default='\e[0m' + __static__Analyze_System_Properties + __static__Print_OS_Report_Title + __static__Print_Report_Of_Requirements_With_Minimum_version 'OS' + __static__Prepare_Binary_Report_Array + __static__Print_Formatted_Binary_Report + __static__Print_Python_Report_Title + __static__Print_Report_Of_Requirements_With_Minimum_version 'Python' +} + +function Is_Python_Requirement_Satisfied() +{ + local requirement name version_specifier + requirement=$1 + # According to PEP-440 this should be the symbols that separate the name from the version + # specifiers -> see https://peps.python.org/pep-0440/#version-specifiers for more info. + name="${requirement%%[~<>=\!]*}" + name="${name%%*([[:space:]])}" # Trim possible trailing spaces + version_specifier="${requirement//${name}/}" + ${HYBRID_python_test_requirement_tool} "${name}" "${version_specifier}" +} + +#=================================================================================================== +# First level of Utility functions for functionality above + +function __static__Parse_Python_Requirements_Into_Global_Array() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty HYBRID_python_requirements_file + Ensure_That_Given_Variables_Are_Set HYBRID_python_requirements + local line comment + while read -r line; do + line=${line%#*} # Remove in-line comments + line=${line##+([[:space:]])} # Remove leading spaces + line=${line%%+([[:space:]])} # Remove trailing spaces + if [[ ${line} =~ ^[[:space:]]*$ ]]; then + continue + fi + case "${line}" in + packaging*) + comment='Required to check Python requirements' + ;; + pyDOE*) + comment='Required in "prepare-scan" mode with LHS enabled' + ;; + PyYAML*) + comment='Required in "do" mode for afterburner with spectators' + ;; + *) + comment='Always required' # This comment is used elsewhere -> rename with care! + ;; + esac + HYBRID_python_requirements["${line}"]="${comment}" + done < "${HYBRID_python_requirements_file}" +} + +function __static__Analyze_System_Properties() +{ + Ensure_That_Given_Variables_Are_Set system_information + local program name return_code + for program in "${!HYBRID_versions_requirements[@]}"; do + if __static__Try_Find_Requirement "${program}"; then + system_information[${program}]='found|' + else + system_information[${program}]='---|---|---' + continue + fi + if ! __static__Try_Find_Version "${program}"; then # This writes to system_information + continue + fi + if __static__Check_Version_Suffices "${program}"; then + system_information[${program}]+='OK' + else + system_information[${program}]+='---' + fi + done + for program in "${HYBRID_programs_just_required[@]}"; do + if __static__Try_Find_Requirement "${program}"; then + system_information["${program}"]='found' + else + system_information["${program}"]='---' + fi + done + for program in "${HYBRID_gnu_programs_required[@]}"; do + if __static__Try_Find_Requirement "${program}"; then + system_information["GNU-${program}"]='found|' + else + system_information["GNU-${program}"]='---|---' + continue + fi + # Handling of function exit code must be done in this form because of errexit mode + return_code=0 + __static__Is_Gnu_Version_In_Use "${program}" || return_code=$? + case "${return_code}" in + 0) + system_information["GNU-${program}"]+='OK' + ;; + 2) + system_information["GNU-${program}"]+='?' + ;; + *) + system_information["GNU-${program}"]+='---' + ;; + esac + done + for name in "${HYBRID_env_variables_required[@]}"; do + # Handling of function exit code must be done in this form because of errexit mode; + # the subshell is needed because the function exits when the variable is unset or empty + return_code=0 + (Ensure_That_Given_Variables_Are_Set_And_Not_Empty "${name}" &> /dev/null) || return_code=$? + if [[ ${return_code} -eq 0 ]]; then + system_information["${name}"]='OK' + else + system_information["${name}"]='---' + fi + done + for program in "${!HYBRID_python_requirements[@]}"; do + # Here the exit code of the requirement check is not relevant and we ignore it with '|| true' + system_information["${program}"]="$(Is_Python_Requirement_Satisfied "${program}" || true)" + done + for program in "${!system_information[@]}"; do + Print_Debug "${program} -> ${system_information[${program}]}" + done +} + +function __static__Exit_If_Some_GNU_Requirement_Is_Missing() +{ + local program errors=0 is_gnu + for program in "${HYBRID_gnu_programs_required[@]}"; do + Ensure_That_Given_Variables_Are_Set_And_Not_Empty "system_information[GNU-${program}]" + is_gnu=$(__static__Get_Field_In_System_Information_String "GNU-${program}" 1) + if [[ ${is_gnu} = '---' ]]; then + Print_Error --emph "${program}" ' either not found or non-GNU version in use.' \ + 'Please, ensure that ' --emph "${program}" ' is installed and in use.' + ((errors++)) || true + fi + done + if [[ ${errors} -ne 0 ]]; then + exit_code=${HYBRID_fatal_missing_requirement} Print_Fatal_And_Exit \ + 'The GNU version of the above program(s) is needed.' + fi +} + +function __static__Exit_If_Minimum_Versions_Are_Not_Available() +{ + local program errors=0 min_version program_found version_found version_ok + for program in "${!HYBRID_versions_requirements[@]}"; do + Ensure_That_Given_Variables_Are_Set_And_Not_Empty "system_information[${program}]" + min_version=${HYBRID_versions_requirements["${program}"]} + program_found=$(__static__Get_Field_In_System_Information_String "${program}" 0) + version_found=$(__static__Get_Field_In_System_Information_String "${program}" 1) + version_ok=$(__static__Get_Field_In_System_Information_String "${program}" 2) + if [[ "${program_found}" = '---' ]]; then + Print_Error --emph "${program}" ' command not found! Minimum version ' \ + --emph "${min_version}" ' is required.' + ((errors++)) || true + continue + fi + if [[ ${version_found} = '---' ]]; then + Print_Warning 'Unable to find version of ' --emph "${program}" ', skipping version check!' \ + 'Please ensure that current version is at least ' --emph "${min_version}" '.' + continue + fi + if [[ "${version_ok}" = '---' ]]; then + Print_Error --emph "${program}" ' version ' --emph "${version_found}" \ + ' found, but version ' --emph "${min_version}" ' is required.' + ((errors++)) || true + fi + done + if [[ ${errors} -ne 0 ]]; then + exit_code=${HYBRID_fatal_missing_requirement} Print_Fatal_And_Exit \ + 'Please install (maybe locally) the required versions of the above program(s).' + fi +} + +function __static__Exit_If_Some_Needed_Environment_Variable_Is_Missing() +{ + local name errors=0 + for name in "${HYBRID_env_variables_required[@]}"; do + Ensure_That_Given_Variables_Are_Set_And_Not_Empty "system_information[${name}]" + if [[ ${system_information[${name}]} = '---' ]]; then + Print_Error --emph "${name}" ' environment variable either unset or empty.' \ + 'Please, ensure that ' --emph "${name}" ' is properly set.' + ((errors++)) || true + fi + done + if [[ ${errors} -ne 0 ]]; then + exit_code=${HYBRID_fatal_missing_requirement} Print_Fatal_And_Exit \ + 'Please, set the above environment variable(s) to appropriate value(s).' + fi +} + +function __static__Exit_If_Some_Always_Needed_Python_Requirement_Is_Missing() +{ + if ! Is_Python_Requirement_Satisfied 'packaging' &> /dev/null; then + Print_Error \ + 'The Python ' --emph 'packaging' ' module is required to check Python requirements.' \ + 'Please install it e.g. via ' --emph 'pip install packaging' '.' \ + 'Then the handler will be able to check Python requirements.' \ + 'Skipping requirements check might lead to unexpected behavior or errors.' '' + return + fi + Ensure_That_Given_Variables_Are_Set_And_Not_Empty \ + HYBRID_execution_mode HYBRID_scan_strategy HYBRID_optional_feature[Add_spectators_from_IC] + local requirement errors=0 package_found version_found version_ok + for requirement in "${!HYBRID_python_requirements[@]}"; do + if [[ "${HYBRID_python_requirements[${requirement}]}" != 'Always required' ]]; then + continue + fi + Ensure_That_Given_Variables_Are_Set_And_Not_Empty "system_information[${requirement}]" + package_found=$(__static__Get_Field_In_System_Information_String "${requirement}" 0) + version_found=$(__static__Get_Field_In_System_Information_String "${requirement}" 1) + version_ok=$(__static__Get_Field_In_System_Information_String "${requirement}" 2) + if [[ "${package_found}" = '?' ]]; then + Print_Warning \ + 'Unable to check Python ' --emph "${requirement}" ' requirement!' \ + 'Please ensure that it is satisfied.' + continue + elif [[ "${package_found}" = '---' ]]; then + Print_Error \ + 'Python requirement ' --emph "${requirement}" \ + ' not found, but needed in this run.' + ((errors++)) || true + continue + fi + if [[ ! ${version_found} =~ ${HYBRID_version_regex} ]]; then + Print_Internal_And_Exit \ + 'Unexpected version value ' --emph "${version_found}" ' found when checking for Python ' \ + --emph "${requirement}" ' requirement.' + fi + if [[ "${version_ok}" = '---' ]]; then + Print_Error \ + 'Python requirement ' --emph "${requirement}" \ + ' not met! Found version ' --emph "${version_found}" ' installed.' + ((errors++)) || true + fi + done + if [[ ${errors} -ne 0 ]]; then + exit_code=${HYBRID_fatal_missing_requirement} Print_Fatal_And_Exit \ + 'Please install the above Python requirement(s).' + fi +} + +function __static__Print_OS_Report_Title() +{ + printf "\e[1m System requirements overview:\e[0m\n\n" +} + +function __static__Print_Python_Report_Title() +{ + printf "\n\e[1m Python requirements overview:\e[0m\n\n" +} + +function __static__Print_Report_Of_Requirements_With_Minimum_version() +{ + local report_string program sorting_column final_newline + # NOTE: sort might not be available, hence put report in string and then optionally sort it + report_string='' + if [[ $1 = 'OS' ]]; then + sorting_column=3 + final_newline='\n' + for program in "${!HYBRID_versions_requirements[@]}"; do + report_string+=$(__static__Print_Requirement_Version_Report_Line "${program}")$'\n' + done + elif [[ $1 = 'Python' ]]; then + sorting_column=2 + final_newline='' + for program in "${!HYBRID_python_requirements[@]}"; do + report_string+=$(__static__Print_Python_Requirement_Report_Line "${program}")$'\n' + done + else + Print_Internal_And_Exit 'Unexpected call of ' --emph "${FUNCNAME}" ' function.' + fi + if hash sort &> /dev/null; then + # The sorting column must take into account hidden color codes seen by sort and + # here we want to sort using either the program/module name; remember that the + # the 'here-string' adds a newline to the string when feeding it into the command. + sort -b -f -k${sorting_column} <<< "${report_string%?}" + else + printf '%s' "${report_string}" + fi + printf "${final_newline}" +} + +function __static__Prepare_Binary_Report_Array() +{ + Ensure_That_Given_Variables_Are_Set system_report + Ensure_That_Given_Variables_Are_Set_And_Not_Empty single_field_length + for name in "${HYBRID_programs_just_required[@]}"; do + system_report+=( + "$( + __static__Get_Single_Tick_Cross_Requirement_Report \ + "PROG ${name}" \ + "${system_information[${name}]}" + )" + ) + done + for program in "${HYBRID_gnu_programs_required[@]}"; do + is_gnu=$(__static__Get_Field_In_System_Information_String "GNU-${program}" 1) + system_report+=( + "$( + __static__Get_Single_Tick_Cross_Requirement_Report \ + "GNU ${program}" \ + "${is_gnu}" + )" + ) + done + for name in "${HYBRID_env_variables_required[@]}"; do + system_report+=( + "$( + __static__Get_Single_Tick_Cross_Requirement_Report \ + "ENV ${name}" \ + "${system_information[${name}]}" + )" + ) + done +} + +function __static__Print_Formatted_Binary_Report() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty system_report + # Because of coloured output, we cannot use a tool like 'column' here to format output + # and we manually determine how many columns to use. Furthermore tput needs the TERM + # environment variable to be set and, as tput is a requirement, we cannot rely on it + # here. Although in some cases this might fail, we refresh and use COLUMNS variable + # here (see https://stackoverflow.com/a/48016366/14967071 for more information). + shopt -s checkwinsize # Do not assume it is on (as it usually is) + (:) # Refresh LINES and COLUMNS, this happens when a child process exits + local -r num_cols=$((${COLUMNS-100} / 2 / single_field_length)) + local index printf_descriptor + printf_descriptor="%${single_field_length}s" # At least one column + for ((index = 1; index < num_cols; index++)); do + printf_descriptor+=" %${single_field_length}s" + done + printf "${printf_descriptor}\n" "${system_report[@]}" +} + +#=================================================================================================== +# Second level of Utility functions for functionality above + +function __static__Try_Find_Requirement() +{ + if hash "$1" 2> /dev/null; then + return 0 + else + return 1 + fi +} + +function __static__Try_Find_Version() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty "system_information[$1]" + if ! hash grep &> /dev/null && [[ $1 != 'bash' ]]; then + system_information["$1"]+='?|---' + return 1 + fi + local found_version + case "$1" in + awk | git | sed | python3) + found_version=$($1 --version) + found_version=$(grep -oE "${HYBRID_version_regex}" <<< "${found_version}") + found_version=$(__static__Get_First_Line_From_String "${found_version}") + ;; + bash) + found_version="${BASH_VERSINFO[@]:0:3}" + found_version="${found_version// /.}" + ;; + tput) + found_version=$(tput -V | grep -oE "${HYBRID_version_regex}") + found_version=(${found_version//./ }) # Use word split to separate version numbers + found_version="${found_version[0]-}.${found_version[1]-}" # Use empty variables if 'tput -V' failed + ;; + yq) + # Versions before v4.30.3 do not have the 'v' prefix + found_version=$(yq --version \ + | grep -oE "version [v]?${HYBRID_version_regex}" \ + | grep -oE "${HYBRID_version_regex}") + ;; + *) + Print_Internal_And_Exit 'Version finding for ' --emph "$1" ' to be added!' + ;; + esac + if [[ ${found_version} =~ ^${HYBRID_version_regex}$ ]]; then + system_information["$1"]+="${found_version}|" + else + system_information["$1"]+='---|---' + return 1 + fi +} + +function __static__Check_Version_Suffices() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty "system_information[$1]" + # Here we assume that the programs were found and their version, too + local program version_required version_found newer_version + program=$1 + version_required="${HYBRID_versions_requirements[${program}]}" + version_found=$(__static__Get_Field_In_System_Information_String "${program}" 1) + # If versions are equal, we're done + if [[ "${version_required}" = "${version_found}" ]]; then + return 0 + fi + newer_version=$(__static__Get_Larger_Version ${version_required} ${version_found}) + if [[ ${newer_version} = ${version_required} ]]; then + return 1 + else + return 0 + fi +} + +function __static__Is_Gnu_Version_In_Use() +{ + # This follows apparently common sense -> https://stackoverflow.com/a/61767587/14967071 + if ! hash grep &> /dev/null || ! "$1" --version &> /dev/null; then + return 2 + elif [[ $("$1" --version | grep -c 'GNU') -gt 0 ]]; then + return 0 + else + return 1 + fi +} + +#=================================================================================================== +# Third level of Utility functions for functionality above + +function __static__Get_Larger_Version() +{ + local v1=$1 v2=$2 dots_of_v1 dots_of_v2 + if [[ ! "${v1}.${v2}" =~ ${HYBRID_version_regex} ]]; then + Print_Internal_And_Exit 'Wrong arguments passed to ' --emph "${FUNCNAME}" '.' + fi + # Ensure versions are of the same length to make following algorithm work + dots_of_v1=${v1//[^.]/} + dots_of_v2=${v2//[^.]/} + if [[ ${#dots_of_v1} -lt ${#dots_of_v2} ]]; then + declare -n \ + shorter_version=v1 dots_of_shorter_version=dots_of_v1 dots_of_longer_version=dots_of_v2 + else + declare -n \ + shorter_version=v2 dots_of_shorter_version=dots_of_v2 dots_of_longer_version=dots_of_v1 + fi + while [[ ${#dots_of_shorter_version} -ne ${#dots_of_longer_version} ]]; do + shorter_version+='.0' # Add zeroes to shorter string + dots_of_shorter_version+='.' # Add dot to keep counting accurate + done + # If versions are equal, we're done + if [[ "${v2}" = "${v1}" ]]; then + printf "${v1}" + return + fi + # Split version strings into array of numbers replacing '.' by ' ' and let word splitting do the split + local v{1,2}_array index + v1_array=(${v1//./ }) + v2_array=(${v2//./ }) + # Now version arrays have same number of entries, compare them + for index in ${!v1_array[@]}; do + if [[ "${v1_array[index]}" -eq "${v2_array[index]}" ]]; then + continue + elif [[ "${v1_array[index]}" -lt "${v2_array[index]}" ]]; then + printf "$2" # Print input version, unchanged + return + else + printf "$1" # Print input version, unchanged + return + fi + done +} + +function __static__Print_Requirement_Version_Report_Line() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty \ + "system_information[$1]" \ + emph_color red green yellow text_color default + local line found version_found version_ok program=$1 + found=$(__static__Get_Field_In_System_Information_String "${program}" 0) + version_found=$(__static__Get_Field_In_System_Information_String "${program}" 1) + version_ok=$(__static__Get_Field_In_System_Information_String "${program}" 2) + # The space after the color code of the command name is important to + # make it separate as 'columns' from the requirement for later sorting. + printf -v line " ${text_color}Command ${emph_color} %8s${text_color}: ${default}" "${program}" + if [[ ${found} = '---' ]]; then + line+="${red}NOT " + else + line+="${green} " + fi + line+=$(printf "found ${text_color}Required version: ${emph_color}%6s${default}" \ + "${HYBRID_versions_requirements[${program}]}") + if [[ ${found} != '---' ]]; then + line+=" ${text_color}System version:${default} " + if [[ ${version_found} =~ ^(---|\?)$ ]]; then + line+="${yellow}Unable to recover" + else + if [[ ${version_ok} = '---' ]]; then + line+="${red}" + else + line+="${green}" + fi + line+="${version_found}" + fi + line+="${default}" + fi + printf "${line}\n" +} + +function __static__Print_Python_Requirement_Report_Line() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty \ + "system_information[$1]" \ + emph_color red green yellow text_color default + local line found version_found version_ok requirement=$1 + found=$(__static__Get_Field_In_System_Information_String "${requirement}" 0) + version_found=$(__static__Get_Field_In_System_Information_String "${requirement}" 1) + version_ok=$(__static__Get_Field_In_System_Information_String "${requirement}" 2) + # The space after the color code of the requirement is important to make it + # separate as 'columns' from the requirement for later sorting. + printf -v line "${emph_color} %18s${text_color}: ${default}" "${requirement}" + if [[ ${found} = '---' ]]; then + line+="${red}✘" + elif [[ ${found} = 'wrong' ]]; then + line+="${yellow}✘" + elif [[ ${found} = '?' ]]; then + line+="${yellow}?" + elif [[ ${version_ok} = '?' ]]; then + line+="${green}?" + else + line+="${green}✔︎" + fi + if [[ ${version_found} = '---' ]]; then + line+=$(printf '%-15s' '') + else + line+=" ${text_color}-> " + if [[ ${version_ok} = 'OK' ]]; then + line+="${green}" + else + line+="${red}" + fi + line+=$(printf "%-9s${default}" "${version_found}") + fi + Print_Debug "${requirement}" + line+="${emph_color}[${HYBRID_python_requirements["${requirement}"]}]${default}" + printf "${line}\n" +} + +function __static__Get_Single_Tick_Cross_Requirement_Report() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty \ + single_field_length emph_color red green yellow text_color default + local line name="$1" status=$2 name_string + printf -v name_string "%s ${emph_color}%s" "${name% *}" "${name#* }" + printf -v line " %*s${text_color}: ${default}" "${single_field_length}" "${name_string}" + if [[ ${status} = '---' ]]; then + line+="${red}✘" + elif [[ ${status} = '?' ]]; then + line+="${yellow}\e[1m?" + else + line+="${green}✔︎" + fi + printf "${line}${default}" +} + +function __static__Get_Field_In_System_Information_String() +{ + Ensure_That_Given_Variables_Are_Set_And_Not_Empty "system_information[$1]" + local tmp_array=(${system_information[$1]//|/ }) # Unquoted to let word splitting act + printf '%s' "${tmp_array[$2]}" +} + +# This is basically a partial bash implementation of head, which we want to +# avoid using in this file as it is a requirement that we want to check +function __static__Get_First_Line_From_String() +{ + while IFS= read -r line; do + printf "${line}" + return + done < <(printf '%s\n' "$1") + # The \n in printf is important to avoid skipping the last line (which might be the only input) +} + +Make_Functions_Defined_In_This_File_Readonly diff --git a/bash/utility_functions.bash b/bash/utility_functions.bash new file mode 100644 index 0000000..d23b024 --- /dev/null +++ b/bash/utility_functions.bash @@ -0,0 +1,592 @@ +#=================================================== +# +# Copyright (c) 2023-2024 +# SMASH Hybrid Team +# +# GNU General Public License (GPLv3 or later) +# +#=================================================== + +# ATTENTION: Unless it makes sense to do it differently, each function here +# should have a short documentation before and then be included in +# the developer guide. Be consistent when adding a new function. +# Here a simple bash trick is used: A no-operation ':' followed by +# a here-doc is used to be able to put the markdown documentation +# inside and still make this be ignored by the shell. In the here-doc +# a couple of "snippet anchors" is used to be able to include the +# snippets in the developer guide. Each snippet begins and ends with +# '--8<-- [start: