From 1faff84113320ddaca242fd132f697ed1543586c Mon Sep 17 00:00:00 2001 From: "Jason W. DeGraw" Date: Mon, 18 May 2026 17:43:06 -0400 Subject: [PATCH 1/7] Translate ReadVars into Python --- CMakeLists.txt | 5 +- cmake/Install.cmake | 3 +- cmake/ProjectMacros.cmake | 12 +- cmake/RunReverseDD.cmake | 4 +- cmake/RunSimulation.cmake | 4 +- scripts/Epl-run.bat | 12 +- scripts/RunEPlus.bat | 12 +- scripts/RunReadESO.bat | 4 +- src/EnergyPlus/CommandLineInterface.cc | 29 +- src/EnergyPlus/FileSystem.cc | 2 +- src/ReadVars/CMakeLists.txt | 61 +- src/ReadVars/ReadVarsESO.bat.in | 10 + src/ReadVars/ReadVarsESO.f90 | 1579 ----------------- src/ReadVars/ReadVarsESO.in | 7 + src/ReadVars/ReadVarsESO.py | 883 +++++++++ .../ExerciseOutput1-CustomCSV.bat | 20 +- 16 files changed, 997 insertions(+), 1650 deletions(-) create mode 100644 src/ReadVars/ReadVarsESO.bat.in delete mode 100644 src/ReadVars/ReadVarsESO.f90 create mode 100644 src/ReadVars/ReadVarsESO.in create mode 100644 src/ReadVars/ReadVarsESO.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 8642a21a64e..218aea2a94f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -376,6 +376,7 @@ if(BUILD_TESTING) endif() add_subdirectory(src/ConvertInputFormat) +add_subdirectory(src/ReadVars) if(BUILD_FORTRAN) include(CMakeAddFortranSubdirectory) @@ -385,10 +386,6 @@ if(BUILD_FORTRAN) src/ExpandObjects PROJECT ExpandObjects NO_EXTERNAL_INSTALL) - cmake_add_fortran_subdirectory( - src/ReadVars - PROJECT ReadVars - NO_EXTERNAL_INSTALL) set(TRANSITION_EXTRA_FLAGS "-DCMAKE_VERSION_MAJOR:STRING=${CMAKE_VERSION_MAJOR}" "-DCMAKE_VERSION_MINOR:STRING=${CMAKE_VERSION_MINOR}" "-DCMAKE_VERSION_PATCH:STRING=${CMAKE_VERSION_PATCH}") cmake_add_fortran_subdirectory( diff --git a/cmake/Install.cmake b/cmake/Install.cmake index 8154db9584e..f7e916bd101 100644 --- a/cmake/Install.cmake +++ b/cmake/Install.cmake @@ -85,7 +85,6 @@ set(CPACK_INSTALL_CMAKE_PROJECTS "${PROJECT_BINARY_DIR};EnergyPlus;ALL;/") if(BUILD_FORTRAN) list(APPEND CPACK_INSTALL_CMAKE_PROJECTS "${PROJECT_BINARY_DIR}/src/ExpandObjects/;ExpandObjects;ALL;/") - list(APPEND CPACK_INSTALL_CMAKE_PROJECTS "${PROJECT_BINARY_DIR}/src/ReadVars/;ReadVars;ALL;/") list(APPEND CPACK_INSTALL_CMAKE_PROJECTS "${PROJECT_BINARY_DIR}/src/Transition/;Transition;ALL;/") list(APPEND CPACK_INSTALL_CMAKE_PROJECTS "${PROJECT_BINARY_DIR}/src/Basement/;Basement;ALL;/") list(APPEND CPACK_INSTALL_CMAKE_PROJECTS "${PROJECT_BINARY_DIR}/src/HVAC-Diagram/;HVAC-Diagram;ALL;/") @@ -699,7 +698,7 @@ include(CPackIFW) #cpack_add_component(AuxiliaryPrograms #DISPLAY_NAME "Auxiliary Programs" -#DESCRIPTION "The suite of Fortran auxiliary programs such as ReadVarsESO, ExpandObjects, etc" +#DESCRIPTION "The suite of auxiliary programs such as ReadVarsESO, ExpandObjects, etc" #REQUIRED #) diff --git a/cmake/ProjectMacros.cmake b/cmake/ProjectMacros.cmake index d64372329d5..bcaff423177 100644 --- a/cmake/ProjectMacros.cmake +++ b/cmake/ProjectMacros.cmake @@ -85,23 +85,21 @@ function(ADD_SIMULATION_TEST) set(ANNUAL_SIMULATION false) endif() - # Note JM 2018-11-23: -r means "Call ReadVarEso", which unless you actually have BUILD_FORTRAN=TRUE shouldn't exist + # -r means "Call ReadVarsESO". if(ANNUAL_SIMULATION) set(ENERGYPLUS_FLAGS "${ADD_SIM_TEST_ENERGYPLUS_FLAGS} -a") else() set(ENERGYPLUS_FLAGS "${ADD_SIM_TEST_ENERGYPLUS_FLAGS} -D") endif() - # Add -r flag if BUILD_FORTRAN is on, regardless of whether we run regression/performance tests - # So that it'll produce the CSV output automatically for convenience + # Preserve the historical test behavior: when auxiliary Fortran tools are enabled, + # also ask EnergyPlus to produce CSV output through ReadVarsESO. if(BUILD_FORTRAN) set(ENERGYPLUS_FLAGS "${ENERGYPLUS_FLAGS} -r") else() - # Now, if you don't have BUILD_FORTRAN, but you actually need that because of regression/performance testing, we issue messages - if(ADD_SIM_TEST_PERFORMANCE) - # For performance testing, it's more problematic, because that'll cut on the ReadVarEso time - message(WARNING "Will not be able to call ReadVarEso unless BUILD_FORTRAN=TRUE, skipping flag -r.") + # For performance testing, changing whether ReadVarsESO is included changes the measured time. + message(WARNING "BUILD_FORTRAN=FALSE, preserving historical behavior and skipping automatic ReadVarsESO execution.") endif() endif() diff --git a/cmake/RunReverseDD.cmake b/cmake/RunReverseDD.cmake index 65eced77f79..c48511117c6 100644 --- a/cmake/RunReverseDD.cmake +++ b/cmake/RunReverseDD.cmake @@ -152,8 +152,8 @@ foreach(M V IN ZIP_LISTS MODE VAR) list(FIND ENERGYPLUS_FLAGS_LIST -r READVARS_RESULT) if("${READVARS_RESULT}" GREATER -1) - find_program(READVARS_EXE ReadVarsESO PATHS "${PRODUCT_PATH}" NO_DEFAULT_PATH NO_CMAKE_ENVIRONMENT_PATH NO_CMAKE_PATH NO_SYSTEM_ENVIRONMENT_PATH - NO_CMAKE_SYSTEM_PATH NO_CMAKE_FIND_ROOT_PATH) + find_program(READVARS_EXE NAMES ReadVarsESO ReadVarsESO.bat PATHS "${PRODUCT_PATH}" NO_DEFAULT_PATH NO_CMAKE_ENVIRONMENT_PATH NO_CMAKE_PATH + NO_SYSTEM_ENVIRONMENT_PATH NO_CMAKE_SYSTEM_PATH NO_CMAKE_FIND_ROOT_PATH) # Move to executable directory execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${READVARS_EXE}" "${EXE_PATH}") endif() diff --git a/cmake/RunSimulation.cmake b/cmake/RunSimulation.cmake index 1c5b2a59125..b8dabbcd0e7 100644 --- a/cmake/RunSimulation.cmake +++ b/cmake/RunSimulation.cmake @@ -163,8 +163,8 @@ if(BUILD_FORTRAN) list(FIND ENERGYPLUS_FLAGS_LIST -r READVARS_RESULT) if("${READVARS_RESULT}" GREATER -1) - find_program(READVARS_EXE ReadVarsESO PATHS "${PRODUCT_PATH}" NO_DEFAULT_PATH NO_CMAKE_ENVIRONMENT_PATH NO_CMAKE_PATH NO_SYSTEM_ENVIRONMENT_PATH - NO_CMAKE_SYSTEM_PATH NO_CMAKE_FIND_ROOT_PATH) + find_program(READVARS_EXE NAMES ReadVarsESO ReadVarsESO.bat PATHS "${PRODUCT_PATH}" NO_DEFAULT_PATH NO_CMAKE_ENVIRONMENT_PATH NO_CMAKE_PATH + NO_SYSTEM_ENVIRONMENT_PATH NO_CMAKE_SYSTEM_PATH NO_CMAKE_FIND_ROOT_PATH) # Move to executable directory execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${READVARS_EXE}" "${EXE_PATH}") endif() diff --git a/scripts/Epl-run.bat b/scripts/Epl-run.bat index 5a08f4f902e..9b5b0c9dd50 100644 --- a/scripts/Epl-run.bat +++ b/scripts/Epl-run.bat @@ -124,8 +124,8 @@ : 5. If available Copy %1.rvi (post processor commands) into Eplusout.inp : If available Copy %1.mvi (post processor commands) into eplusmtr.inp : or create appropriate input to get meter output from eplusout.mtr -: 6. Execute ReadVarsESO.exe (the Post Processing Program) -: Execute ReadVarsESO.exe (the Post Processing Program) for meter output +: 6. Execute ReadVarsESO (the Post Processing Program) +: Execute ReadVarsESO (the Post Processing Program) for meter output : 7. Copy Eplusout.* to %1.* : 8. Clean up directory. : @@ -386,12 +386,12 @@ IF EXIST ip.err DEL ip.err IF EXIST convert.txt DEL convert.txt :skipConv -IF EXIST eplusout.inp "%program_path%postprocess\ReadVarsESO.exe" eplusout.inp %rvset% -IF NOT EXIST eplusout.inp "%program_path%postprocess\ReadVarsESO.exe" " " %rvset% -IF EXIST eplusmtr.inp "%program_path%postprocess\ReadVarsESO.exe" eplusmtr.inp %rvset% +IF EXIST eplusout.inp "%program_path%postprocess\ReadVarsESO.bat" eplusout.inp %rvset% +IF NOT EXIST eplusout.inp "%program_path%postprocess\ReadVarsESO.bat" " " %rvset% +IF EXIST eplusmtr.inp "%program_path%postprocess\ReadVarsESO.bat" eplusmtr.inp %rvset% IF NOT EXIST eplusmtr.inp echo eplusout.mtr >test.mvi IF NOT EXIST eplusmtr.inp echo eplusmtr.csv >>test.mvi -IF NOT EXIST eplusmtr.inp "%program_path%postprocess\ReadVarsESO.exe" test.mvi %rvset% +IF NOT EXIST eplusmtr.inp "%program_path%postprocess\ReadVarsESO.bat" test.mvi %rvset% :skipReadVars "%program_path%postprocess\HVAC-Diagram.exe" diff --git a/scripts/RunEPlus.bat b/scripts/RunEPlus.bat index bc9bee1c6cf..351b74ae563 100644 --- a/scripts/RunEPlus.bat +++ b/scripts/RunEPlus.bat @@ -59,10 +59,10 @@ : Run the Slab preprocessor program if necessary : 5. Execute EnergyPlus : 6. If available Copy %1.rvi (post processor commands) into Eplusout.inp -: 7. Execute ReadVarsESO.exe (the Post Processing Program) +: 7. Execute ReadVarsESO (the Post Processing Program) : 8. If available Copy %1.mvi (post processor commands) into test.mvi : or create appropriate input to get meter output from eplusout.mtr -: 9. Execute ReadVarsESO.exe (the Post Processing Program) for meter output +: 9. Execute ReadVarsESO (the Post Processing Program) for meter output : 10. Copy Eplusout.* to %1.* : 11. Clean up working directory. : @@ -324,12 +324,12 @@ if %maxcol%==nolimit SET rvset=unlimited : readvars creates audit in append mode. start it off echo %date% %time% ReadVars >readvars.audit -IF EXIST eplusout.inp %post_proc%ReadVarsESO.exe eplusout.inp %rvset% -IF NOT EXIST eplusout.inp %post_proc%ReadVarsESO.exe " " %rvset% -IF EXIST eplusmtr.inp %post_proc%ReadVarsESO.exe eplusmtr.inp %rvset% +IF EXIST eplusout.inp %post_proc%ReadVarsESO.bat eplusout.inp %rvset% +IF NOT EXIST eplusout.inp %post_proc%ReadVarsESO.bat " " %rvset% +IF EXIST eplusmtr.inp %post_proc%ReadVarsESO.bat eplusmtr.inp %rvset% IF NOT EXIST eplusmtr.inp echo eplusout.mtr >test.mvi IF NOT EXIST eplusmtr.inp echo eplusmtr.csv >>test.mvi -IF NOT EXIST eplusmtr.inp %post_proc%ReadVarsESO.exe test.mvi %rvset% +IF NOT EXIST eplusmtr.inp %post_proc%ReadVarsESO.bat test.mvi %rvset% :skipReadVars IF EXIST eplusout.bnd %post_proc%HVAC-Diagram.exe diff --git a/scripts/RunReadESO.bat b/scripts/RunReadESO.bat index dad3bbb5bfe..3ca0ab73505 100644 --- a/scripts/RunReadESO.bat +++ b/scripts/RunReadESO.bat @@ -1,11 +1,11 @@ set rvpath= if EXIST eplusout.inp goto :inp rem produces all variables in .eso file to .csv -%rvpath%readvarseso.exe +%rvpath%ReadVarsESO.bat goto :done :inp rem reads variable specifications from input file -%rvpath%ReadVarsESO.exe eplusout.inp +%rvpath%ReadVarsESO.bat eplusout.inp :done set rvpath= diff --git a/src/EnergyPlus/CommandLineInterface.cc b/src/EnergyPlus/CommandLineInterface.cc index 84d1d8e43e4..7d41535990f 100644 --- a/src/EnergyPlus/CommandLineInterface.cc +++ b/src/EnergyPlus/CommandLineInterface.cc @@ -883,18 +883,35 @@ state.dataStrGlobals->inputFilePath='{:g}', int runReadVarsESO(EnergyPlusData &state) { - fs::path readVarsPath = (state.dataStrGlobals->exeDirectoryPath / "ReadVarsESO").replace_extension(FileSystem::exeExtension); + auto findReadVarsPath = [](fs::path const &directory) -> fs::path { + std::vector candidates; +#ifdef _WIN32 + candidates.emplace_back(directory / "ReadVarsESO.bat"); + candidates.emplace_back(directory / "ReadVarsESO.exe"); +#else + candidates.emplace_back(directory / "ReadVarsESO"); +#endif + for (auto const &candidate : candidates) { + if (FileSystem::fileExists(candidate)) { + return candidate; + } + } + return {}; + }; + + fs::path readVarsPath = findReadVarsPath(state.dataStrGlobals->exeDirectoryPath); - if (!FileSystem::fileExists(readVarsPath)) { - readVarsPath = (state.dataStrGlobals->exeDirectoryPath / "PostProcess" / "ReadVarsESO").replace_extension(FileSystem::exeExtension); - if (!FileSystem::fileExists(readVarsPath)) { + if (readVarsPath.empty()) { + readVarsPath = findReadVarsPath(state.dataStrGlobals->exeDirectoryPath / "PostProcess"); + if (readVarsPath.empty()) { // should report the error differently if the user is calling into E+ through EXE or DLL if (state.dataGlobal->eplusRunningViaAPI) { DisplayString( state, - "ERROR: Could not find ReadVarsESO executable. When calling through C API, make sure to call setEnergyPlusRootDirectory"); + "ERROR: Could not find ReadVarsESO program. When calling through C API, make sure to call setEnergyPlusRootDirectory"); } else { - DisplayString(state, fmt::format("ERROR: Could not find ReadVarsESO executable: {}.", FileSystem::getAbsolutePath(readVarsPath))); + DisplayString(state, fmt::format("ERROR: Could not find ReadVarsESO program under: {}.", + FileSystem::getAbsolutePath(state.dataStrGlobals->exeDirectoryPath))); } return static_cast(ReturnCodes::Failure); } diff --git a/src/EnergyPlus/FileSystem.cc b/src/EnergyPlus/FileSystem.cc index 8b74c535a30..6093aa013e9 100644 --- a/src/EnergyPlus/FileSystem.cc +++ b/src/EnergyPlus/FileSystem.cc @@ -314,7 +314,7 @@ namespace FileSystem { // Note: on Windows, system(command) will already send the command through "cmd /C command" // cf C:\Program Files (x86)\Windows Kits\10\Source\10.0.17763.0\ucrt\exec // Ends up calling something that looks like the following: - // cmd /C ""C:\path\to\ReadVarsESO.exe" "A folder with spaces\1ZoneUncontrolled.mvi" unlimited" + // cmd /C ""C:\path\to\ReadVarsESO.bat" "A folder with spaces\1ZoneUncontrolled.mvi" unlimited" return system(("\"" + command + "\"").c_str()); #else return system(command.c_str()); diff --git a/src/ReadVars/CMakeLists.txt b/src/ReadVars/CMakeLists.txt index e0e9158993d..786053bfeac 100644 --- a/src/ReadVars/CMakeLists.txt +++ b/src/ReadVars/CMakeLists.txt @@ -1,33 +1,48 @@ -project(ReadVars Fortran) +project(ReadVars NONE) cmake_minimum_required(VERSION 3.5.1...4.0.3) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/../../Products") -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/../../Products") -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/../../Products") +set(READVARS_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/ReadVarsESO.py") +set(READVARS_ROOT_OUTPUT_DIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") -set(SRC ReadVarsESO.f90) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/ReadVarsESO.in" "${CMAKE_CURRENT_BINARY_DIR}/ReadVarsESO" @ONLY NEWLINE_STYLE UNIX) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/ReadVarsESO.bat.in" "${CMAKE_CURRENT_BINARY_DIR}/ReadVarsESO.bat" @ONLY NEWLINE_STYLE WIN32) -set(CMAKE_SKIP_RPATH ${FORTRAN_SKIP_RPATH}) -set(SKIP_BUILD_RPATH true) +if(WIN32) + set(READVARS_WRAPPER "${CMAKE_CURRENT_BINARY_DIR}/ReadVarsESO.bat") + set(READVARS_WRAPPER_NAME "ReadVarsESO.bat") +else() + set(READVARS_WRAPPER "${CMAKE_CURRENT_BINARY_DIR}/ReadVarsESO") + set(READVARS_WRAPPER_NAME "ReadVarsESO") +endif() + +set(READVARS_CHMOD_ROOT_COMMAND) +set(READVARS_CHMOD_CONFIG_COMMAND) +if(UNIX) + set(READVARS_CHMOD_ROOT_COMMAND COMMAND chmod +x "${READVARS_ROOT_OUTPUT_DIR}/${READVARS_WRAPPER_NAME}") + set(READVARS_CHMOD_CONFIG_COMMAND COMMAND chmod +x "${READVARS_ROOT_OUTPUT_DIR}/$/${READVARS_WRAPPER_NAME}") +endif() -add_executable(ReadVarsESO ${SRC}) +add_custom_target( + ReadVarsESO ALL + COMMAND "${CMAKE_COMMAND}" -E make_directory "${READVARS_ROOT_OUTPUT_DIR}" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${READVARS_SCRIPT}" "${READVARS_ROOT_OUTPUT_DIR}/ReadVarsESO.py" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${READVARS_WRAPPER}" "${READVARS_ROOT_OUTPUT_DIR}/${READVARS_WRAPPER_NAME}" + ${READVARS_CHMOD_ROOT_COMMAND} + SOURCES "${READVARS_SCRIPT}" "${CMAKE_CURRENT_SOURCE_DIR}/ReadVarsESO.in" "${CMAKE_CURRENT_SOURCE_DIR}/ReadVarsESO.bat.in" + VERBATIM) set_target_properties(ReadVarsESO PROPERTIES FOLDER Auxiliary) -if(NOT UNIX) # Need to reinclude it on Windows - include("${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/Fortran.cmake") -endif() -get_target_property(F_INTERFACE_COMPILE_FLAGS fortran_project_options INTERFACE_COMPILE_OPTIONS) -message(DEBUG "ReadVars: F_INTERFACE_COMPILE_FLAGS=${F_INTERFACE_COMPILE_FLAGS}") -target_link_libraries(ReadVarsESO PRIVATE fortran_project_options) - -install(TARGETS ReadVarsESO DESTINATION PostProcess) -if(NOT ${FORTRAN_STATIC_EXE}) - include("${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/ProjectMacros.cmake") - install_target_prereqs(ReadVarsESO PostProcess) +if(CMAKE_CONFIGURATION_TYPES) + add_custom_command( + TARGET ReadVarsESO + POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E make_directory "${READVARS_ROOT_OUTPUT_DIR}/$" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${READVARS_SCRIPT}" "${READVARS_ROOT_OUTPUT_DIR}/$/ReadVarsESO.py" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${READVARS_WRAPPER}" "${READVARS_ROOT_OUTPUT_DIR}/$/${READVARS_WRAPPER_NAME}" + ${READVARS_CHMOD_CONFIG_COMMAND} + VERBATIM) endif() -if(APPLE AND CPACK_CODESIGNING_DEVELOPPER_ID_APPLICATION) - include("${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/CodeSigning.cmake") - register_install_codesign_target(ReadVarsESO PostProcess Unspecified) -endif() +install(FILES "${READVARS_SCRIPT}" DESTINATION PostProcess) +install(PROGRAMS "${READVARS_WRAPPER}" DESTINATION PostProcess) diff --git a/src/ReadVars/ReadVarsESO.bat.in b/src/ReadVars/ReadVarsESO.bat.in new file mode 100644 index 00000000000..652efaf5531 --- /dev/null +++ b/src/ReadVars/ReadVarsESO.bat.in @@ -0,0 +1,10 @@ +@echo off +setlocal + +where python >NUL 2>NUL +if %ERRORLEVEL% EQU 0 ( + python "%~dp0ReadVarsESO.py" %* +) else ( + py -3 "%~dp0ReadVarsESO.py" %* +) +exit /b %ERRORLEVEL% diff --git a/src/ReadVars/ReadVarsESO.f90 b/src/ReadVars/ReadVarsESO.f90 deleted file mode 100644 index dfacb742d0e..00000000000 --- a/src/ReadVars/ReadVarsESO.f90 +++ /dev/null @@ -1,1579 +0,0 @@ - program readvarseso - implicit none -! This program reads the eso file -! the eso file has the format: -! that look like: -! 6,2,Environment,Outdoor Dry Bulb [C] !Hourly -! 978,2,PSI FOYER,Mean Air Temperature[C] !Hourly -! 979,2,DORM ROOMS AND COMMON AREAS,Mean Air Temperature[C] !Hourly -! 980,2,LEFT FORK,Mean Air Temperature[C] !Hourly -! 981,2,MIDDLE FORK,Mean Air Temperature[C] !Hourly -! 982,2,RIGHT FORK,Mean Air Temperature[C] !Hourly -! End of Data Dictionary -! that look like: -! 1,COLORADO SPRINGS COLORADO WINTER, 40.13, -88.13, -6.00, 229.50 -! 2, 1, 1,21, 0, 1, 0.00,60.00,Monday -! 6,-16.66667 -! 714,1.478209 -! 716,16.12879 -! 718,14.27697 -! ... -! End of Data - - character(len=*),parameter :: blank=' ' - character(len=*),parameter :: cTab=Char(9) - integer,parameter :: maxnamelength=500 - character(len=*),parameter,dimension(12) :: months=(/'January ','February ','March ', & - 'April ','May ','June ', & - 'July ','August ','September', & - 'October ','November ','December '/) - integer,parameter,dimension(12) :: monlen=(/ & - len_trim('January'),len_trim('February'),len_trim('March'), & - len_trim('April'),len_trim('May'),len_trim('June'), & - len_trim('July'),len_trim('August'),len_trim('September'), & - len_trim('October'),len_trim('November'),len_trim('December')/) - character(len=1), parameter :: charcomma=char(44) !comma - character(len=1), parameter :: chartab=char(9) !tab - character(len=1), parameter :: charspace=char(32) !space - character(len=500) varfilename - character(len=*), parameter :: inoutformat='(A)' - character(len=*), parameter :: dateformat="(1x,i2.2,'/',i2.2,2x,i2.2,':',i2.2,':',i2.2)" - ! Lengths, including extra following "," - integer, parameter :: curperlen=12 - integer, parameter :: curdaylen=7 - integer, parameter :: curmdhlen=17 - integer :: curmonlen ! Dependent on which month - - integer :: curnummax=10000 - integer :: curfnummax=10000 - integer :: cursnummax=10000 - integer :: incnum=1000 - integer, parameter :: numallowed=255 - character(len=500) inputfilename - character(len=500) outputfilename - integer esounit,csvunit - character(len=500) line - integer i,lineno,j - integer ij - integer k,ik - integer numtoskip - logical :: getvarsfromeso=.false. - - integer :: ntrack=0 - integer :: nignore=0 - integer,allocatable,dimension(:) :: tracknum - integer,allocatable,dimension(:) :: ignorenum - integer,allocatable,dimension(:) :: temptracknum - character(len=maxnamelength*3),allocatable,dimension(:) :: trackvar - character(len=maxnamelength*3),allocatable,dimension(:) :: ignorevar - logical,allocatable,dimension(:) :: trackfound - character(len=maxnamelength*3),allocatable,dimension(:) :: temptrackvar - logical,allocatable,dimension(:) :: temptrackfound - - character(len=25),allocatable,dimension(:) :: outdata - logical,allocatable,dimension(:) :: outfound - character(len=maxnamelength*3),allocatable,dimension(:) :: findvar - integer,allocatable,dimension(:) :: findvarprocessed - character(len=maxnamelength*3),allocatable,dimension(:) :: ignorefindvar - logical,allocatable,dimension(:) :: findvarnumeric - character(len=maxnamelength*3),allocatable,dimension(:) :: tempfindvar - integer,allocatable,dimension(:) :: tempfindvarprocessed - logical,allocatable,dimension(:) :: tempfindvarnumeric - character(len=maxnamelength*3) tempvar - integer :: ntofind=0 - integer :: ntoignore=0 - character(len=maxnamelength*3),allocatable,dimension(:) :: stovar - integer,allocatable,dimension(:) :: stonum - logical,allocatable,dimension(:) :: stofound - character(len=maxnamelength*3),allocatable,dimension(:) :: tempstovar - integer,allocatable,dimension(:) :: tempstonum - logical,allocatable,dimension(:) :: tempstofound - integer nstore - character(len=15) LineArg - character(len=1) sepvar - character(len=3) :: fileextension=blank - - integer iform,dayofsim,month,day,hourofday,dstind - real sminute,eminute - real nsminute,neminute - integer nday,nmonth,nhourofday - character(len=30) curdate,curmonday,curmon,curper - logical :: nodetails=.true. - logical :: nomonday=.true. - logical :: nomon=.true. - integer curhr,curmin,cursec - character(len=145) outlinepart - character(len=3000) outline - logical done - logical havehourly - logical havetimestep - logical reachedend - logical anytoprint - integer arg - integer ArgNum - integer cmdargs - integer rviunit - integer auditunit - logical esothere - logical rvithere - integer ios - logical gotinputfilename - logical gotoutputfilename - integer, allocatable, dimension(:) :: trackindex - integer maxrptnum - integer ii - logical :: ErrorsHappened=.false. - logical :: useoutline - - CHARACTER(len=25) Elapsed - INTEGER Hours ! Elapsed Time Hour Reporting - INTEGER Minutes ! Elapsed Time Minute Reporting - REAL Seconds ! Elapsed Time Second Reporting - REAL Time_Start - REAL Time_Finish - DOUBLE PRECISION :: Elapsed_Time=0.0 ! For showing elapsed tiem at end of run - INTEGER :: Freqs=0 - logical :: limited=.true. - logical :: ignorethisone=.false. - integer :: kpos - integer :: pass - integer :: pos - integer :: epos ! exclamation position, if any - integer :: p1 - logical :: TabsInLine - character(len=30) cdayofsim - integer :: commacount - integer :: commalimit - logical :: FixHeader - - write(*,*) 'ReadVarsESO program starting.' - maxrptnum=-1 - rviunit=20 - esounit=21 - csvunit=22 - auditunit=25 - done=.false. - ntrack=0 - havehourly=.false. - havetimestep=.false. - reachedend=.false. - ntofind=0 - Freqs=0 - gotinputfilename=.false. - gotoutputfilename=.false. - FixHeader=.false. - - commalimit=LEN(outline)-10 - - cmdargs=Command_Argument_Count() - if (cmdargs == 0) then - getvarsfromeso=.true. - else ! there are command args, at least one... - arg=1 - Call Get_Command_Argument(arg,varfilename) - varfilename=ADJUSTL(varfilename) - if (varfilename == blank) then - getvarsfromeso=.true. - endif - argnum=arg+1 - Freqs=0 - do arg=argnum,cmdargs - Call Get_Command_Argument(arg,LineArg) - ! timestep - if (LineArg(1:1) == 't' .or. LineArg(1:1) == 'T') Freqs=1 - ! detailed - if (LineArg(1:2) == 'de' .or. LineArg(1:2) == 'De' .or. LineArg(1:2) == 'DE') Freqs=1 - ! hourly - if (LineArg(1:1) == 'h' .or. LineArg(1:1) == 'H') Freqs=2 - ! daily - if (LineArg(1:2) == 'da' .or. LineArg(1:2) == 'Da' .or. LineArg(1:2) == 'DA') Freqs=3 - ! monthly - if (LineArg(1:1) == 'm' .or. LineArg(1:1) == 'M') Freqs=4 - ! annual or runperiod - if (LineArg(1:1) == 'a' .or. LineArg(1:1) == 'A' .or. LineArg(1:1) == 'r' .or. LineArg(1:1) == 'R') Freqs=5 - ! unlimited - if (LineArg(1:1) == 'u' .or. LineArg(1:1) == 'U') Limited=.false. - ! nolimit - if (LineArg(1:1) == 'n' .or. LineArg(1:1) == 'N') Limited=.false. - ! fixheader - if (LineArg(1:1) == 'f' .or. LineArg(1:1) == 'F') FixHeader=.true. - enddo - endif - - CALL CPU_TIME(Time_Start) - -!!! Determine where to find the variables names to extract from the source variable file - open(auditunit,file='readvars.audit',position='APPEND') - write(auditunit,*) 'ReadVarsESO' - - - if (.not. getvarsfromeso) then - inquire(file=varfilename,exist=rvithere) - if (.not. rvithere) goto 904 - write(auditunit,*) 'processing:',trim(varfilename) - OPEN(rviunit,file=varfilename) - ! write(*,*) 'Enter file name with values, for eplusout.eso' - do while (.not. gotinputfilename) - read(rviunit,inoutformat,iostat=ios) inputfilename - if (ios /= 0) then ! end of file - write(auditunit,'(A)') ' reached end of rvi file while looking for input file name' - inputfilename='eplusout.eso' - outputfilename='eplusout.csv' - getvarsfromeso=.true. - gotoutputfilename=.true. - sepvar=charcomma - exit - endif - inputfilename=adjustl(inputfilename) - if (inputfilename(1:1) == '!') then - write(auditunit,'(A)') ' ignoring comment line='//trim(inputfilename) - cycle ! ignore that are comments - endif - epos=scan(inputfilename,'!') - if (epos /= 0 .and. epos /= 1) then - write(auditunit,'(A)') 'comment stripped on line:'//trim(inputfilename) - inputfilename=inputfilename(1:epos-1) - elseif (epos == 1) then ! this can't happen due to above. - inputfilename=blank - endif - gotinputfilename=.true. - enddo - if (inputfilename == blank) then - inputfilename='eplusout.eso' - endif - inquire(file=trim(inputfilename),exist=esothere) - if (.not. esothere) goto 903 - write(auditunit,*) 'input file:',trim(inputfilename) - open(esounit,file=trim(inputfilename)) - ! write(*,*) 'Enter file name for output, for eplusout.csv' - do while (.not. gotoutputfilename) - read(rviunit,inoutformat) outputfilename - outputfilename=adjustl(outputfilename) - if (outputfilename(1:1) == '!') then - write(auditunit,'(A)') ' ignoring comment line='//trim(outputfilename) - cycle ! ignore that are comments - endif - epos=scan(outputfilename,'!') - if (epos /= 0 .and. epos /= 1) then - write(auditunit,'(A)') 'comment stripped on line:'//trim(outputfilename) - outputfilename=outputfilename(1:epos-1) - elseif (epos == 1) then - outputfilename=blank - endif - gotoutputfilename=.true. - enddo - if (outputfilename == blank) then - outputfilename='eplusout.csv' - sepvar=charcomma - else - ! check last three chars for extension - fileextension=MakeUPPERCase(outputfilename(len_trim(outputfilename)-2:)) - if (fileextension == 'CSV') then - sepvar=charcomma - elseif (fileextension == 'TAB') then - sepvar=chartab - elseif (fileextension == 'TXT') then - sepvar=charspace - else - sepvar=charcomma ! default is still comma - endif - endif - open(csvunit,file=trim(outputfilename),iostat=ios) ! this file can be read into excel - if (ios /= 0) goto 905 - write(auditunit,*) 'output file:',trim(outputfilename) - ! Check out and see if next read is EOF - if (.not. getvarsfromeso) then ! this set if all comments in rvi file. - read(rviunit,inoutformat,iostat=ios) line - else - ios=1 - endif - if (ios /= 0) then - getvarsfromeso=.true. - else - line=adjustl(line) - if (line == blank) then - getvarsfromeso=.true. - elseif (line == '0') then - getvarsfromeso=.true. - else - backspace(unit=rviunit) - endif - endif - else - inputfilename='eplusout.eso' - inquire(file=trim(inputfilename),exist=esothere) - if (.not. esothere) goto 903 - open(esounit,file=trim(inputfilename)) - outputfilename='eplusout.csv' - sepvar=charcomma - open(csvunit,file=trim(outputfilename),iostat=ios) ! this file can be read into excel - if (ios /= 0) goto 905 - endif - -!!! Now, count how many there are (match with source file) up to "end of data dictionary' - - allocate(tracknum(curnummax)) - tracknum=0 - allocate(ignorenum(nignore)) - ignorenum=0 - allocate(findvar(curfnummax)) - findvar=blank - allocate(findvarprocessed(curfnummax)) - findvarprocessed=0 - allocate(findvarnumeric(curfnummax)) - findvarnumeric=.true. - allocate(ignorefindvar(ntoignore)) - ignorefindvar=blank - allocate(stonum(cursnummax)) - stonum=0 - allocate(stofound(cursnummax)) - stofound=.false. - allocate(stovar(cursnummax)) - stovar=blank - - if (.not. getvarsfromeso) then - ! write(*,*) 'You currently have to know which variables to produce.' - ! write(*,*) 'These will appear at the first part of the eplusout.std file.' - - do while (.not. done) ! read the rvi file, sort the variable names entered into ignore/not ignore - ! write(*,*) 'Which variable number to produce?, 0 to quit' - read(rviunit,inoutformat,iostat=ios) line - if (ios /= 0) exit ! end of file - p1=SCAN(line,cTab) - TabsInLine=.false. - DO WHILE (p1>0) - TabsInLine=.true. - line(p1:p1)=' ' - p1=SCAN(line,cTab) - ENDDO - line=adjustl(line) - epos=scan(line,'!') - if (epos /= 0 .and. epos /= 1) then - write(auditunit,'(A)') ' stripping comment from line='//trim(line) - line=line(1:epos-1) - elseif (epos == 1) then - write(auditunit,'(A)') ' ignoring comment line='//trim(line) - cycle ! exclamation makes entire line a comment. - endif - if (index(line,',') == 0) then - if (line(1:1) == '~') then - ignorethisone=.true. - i=processnumber(line(2:)) - else - ignorethisone=.false. - i=processnumber(line) - endif - else - i=-999 - endif - if (line == blank) i=0 - if (i > 0) then - if (.not. ignorethisone) then - ntrack=ntrack+1 - if (ntrack > curnummax) then -! debug write(auditunit,*) '370: reallocate to curnummax+incnum',curnummax,incnum - allocate(temptracknum(curnummax+incnum)) - temptracknum=0 - temptracknum(1:curnummax)=tracknum(1:curnummax) - curnummax=curnummax+incnum - deallocate(tracknum) - allocate(tracknum(curnummax)) - tracknum=temptracknum - deallocate(temptracknum) - endif - - if (ntrack > numallowed) then - if (limited) then - ntrack=numallowed - write(*,*) 'too many variables requested, will go with first ',numallowed - write(auditunit,*) 'too many variables requested, will go with first ',numallowed - exit - endif - endif - tracknum(ntrack)=i - else ! number to be ignored - nignore=nignore+1 - allocate(temptracknum(nignore)) - temptracknum=0 - if (nignore > 1) then - temptracknum(1:nignore-1)=ignorenum(1:nignore-1) - endif - deallocate(ignorenum) - allocate(ignorenum(nignore)) - ignorenum=temptracknum - deallocate(temptracknum) - ignorenum(nignore)=i - endif - else ! variable name entered - if (i < 0) then - if (line(1:1) == '~') then - ignorethisone=.true. - pos=index(line,'[') - if (pos /= 0) then - ! Without the indexes on the LHS, the compiler is complaining that we are squeezing a character position out - ! We add these and it works, but it could potentially leave a trailing character from the prior string - ! So we'll add a space to that trailing point - line(1:pos-2)=line(2:pos-1) - line(pos-1:len(line)) = ' ' - else - line=line(2:) - endif - ntoignore=ntoignore+1 - allocate(tempfindvar(ntoignore)) - tempfindvar=blank - if (ntoignore > 1) then - tempfindvar(1:ntoignore-1)=ignorefindvar(1:ntoignore-1) - endif - deallocate(ignorefindvar) - allocate(ignorefindvar(ntoignore)) - ignorefindvar=tempfindvar - deallocate(tempfindvar) - tempvar=adjustl(line) - ii=index(tempvar,',') - if (ii /= 0) then - ignorefindvar(ntoignore)=tempvar(1:ii) - tempvar=tempvar(ii+1:) - tempvar=adjustl(tempvar) - ignorefindvar(ntoignore)=trim(ignorefindvar(ntoignore))//tempvar - else - ignorefindvar(ntoignore)=tempvar - endif - else - ignorethisone=.false. - endif - if (.not. ignorethisone) then - ntofind=ntofind+1 - if (ntofind > curfnummax) then -! debug write(auditunit,*) '439: reallocate to curfnummax+incnum',curfnummax,incnum - allocate(tempfindvar(curfnummax+incnum)) - tempfindvar=blank - tempfindvar(1:curfnummax)=findvar(1:curfnummax) - allocate(tempfindvarnumeric(curfnummax+incnum)) - tempfindvarnumeric=.true. - tempfindvarnumeric(1:curfnummax)=findvarnumeric(1:curfnummax) - allocate(tempfindvarprocessed(curfnummax+incnum)) - tempfindvarprocessed=0 - tempfindvarprocessed(1:curfnummax)=findvarprocessed(1:curfnummax) - curfnummax=curfnummax+incnum - deallocate(findvar) - deallocate(findvarnumeric) - allocate(findvar(curfnummax)) - findvar=tempfindvar - deallocate(tempfindvar) - allocate(findvarnumeric(curfnummax)) - findvarnumeric=tempfindvarnumeric - deallocate(tempfindvarnumeric) - deallocate(findvarprocessed) - allocate(findvarprocessed(curfnummax)) - findvarprocessed=tempfindvarprocessed - deallocate(tempfindvarprocessed) - endif - pos=index(line,'[') - if (pos /= 0) then - tempvar=line(1:pos-1) - else - tempvar=line - endif - tempvar=adjustl(tempvar) - ii=index(tempvar,',') - if (ii /= 0) then - findvar(ntofind)=tempvar(1:ii) - tempvar=tempvar(ii+1:) - tempvar=adjustl(tempvar) - findvar(ntofind)=trim(findvar(ntofind))//tempvar - findvarnumeric(ntofind)=.false. - else - findvar(ntofind)=tempvar - findvarnumeric(ntofind)=.false. - endif -! findvar(ntofind)=adjustl(line) - endif - else - done=.true. - endif - endif - enddo - if (ntrack == 0 .and. ntofind == 0 .and. nignore == 0 .and. ntoignore == 0) then - write(*,*) 'You chose no variables' - write(auditunit,*) 'You chose no variables' - goto 900 - endif - if (ntrack == 0 .and. ntofind == 0) then - getvarsfromeso=.true. ! except the ones to ignore - endif - endif - - if (getvarsfromeso) then - write(auditunit,*) 'getting all vars from:',trim(inputfilename) - endif - - numtoskip=7 - read(esounit,inoutformat,end=901) line - if (index(line,'Program Version') == 0) numtoskip=6 - do i=1,numtoskip - read(esounit,inoutformat,end=901) line - enddo - nstore=0 - - allocate(trackfound(curnummax)) - trackfound=.false. - allocate(trackvar(curnummax)) - trackvar=blank - - ! pass through eso/input file and look for variables. - pass=1 - do while (pass <= 2) - do while (line /= 'End of Data Dictionary') - i=index(line,',') - if (i == 0) exit - read(line(1:i-1),*) j - if (j > maxrptnum) maxrptnum=j - if (Freqs /= 0) then - select case(Freqs) - case(1) ! Timestep - if (index(line,'!TimeStep') == 0) then - read(esounit,inoutformat,end=901) line - CYCLE - endif - case(2) ! Hourly - if (index(line,'!Hourly') == 0) then - read(esounit,inoutformat,end=901) line - CYCLE - endif - case(3) ! Daily - if (index(line,'!Daily') == 0) then - read(esounit,inoutformat,end=901) line - CYCLE - endif - case(4) ! Monthly - if (index(line,'!Monthly') == 0) then - read(esounit,inoutformat,end=901) line - CYCLE - endif - case(5) ! Annual/RunPeriod - if (index(line,'!RunPeriod') == 0) then - read(esounit,inoutformat,end=901) line - CYCLE - endif - case default - end select - endif - - ! see if on ignore list - if (ANY(ignorenum == j)) then - read(esounit,inoutformat,end=901) line - CYCLE - endif - k=0 - do ij=1,ntoignore - k=myindex(line,trim(ignorefindvar(ij))) - if (k /= 0) exit - enddo - if (k /= 0) then - read(esounit,inoutformat,end=901) line - CYCLE - endif - - if (.not. getvarsfromeso) then - if (ntofind > 0) then - vara:do ij=1,ntofind - if (pass == 2 .and. findvarprocessed(ij) > 0) cycle - k=myindex(line,trim(findvar(ij))) - if (k>0) then - if (line(k-1:k-1) /= ',') k=0 - endif - if (k /= 0) then - kpos=index(line(k:),'[') - if (pass == 1) then - if (kpos /= 0) then - ! if this is not exactly the variable, ignore for now - if (MakeUPPERCase(findvar(ij)) /= MakeUPPERCase(line(k:k+kpos-2))) then - k=0 - endif - else - kpos=index(line(k:),'!') - if (kpos /= 0) then - ! if this is not exactly the variable, ignore for now - if (MakeUPPERCase(findvar(ij)) /= MakeUPPERCase(line(k:k+kpos-2))) then - k=0 - endif - endif - endif - endif - endif - - ik=0 - if (k /= 0) then - if (pass == 1) findvarprocessed(ij)=findvarprocessed(ij)+1 - if (any(tracknum == j))exit vara - if (any(stonum == j))exit vara -! do ik=1,ntrack -! if (tracknum(ik) == j) exit vara -! enddo - ik=0 - if (pass == 2) findvarprocessed(ij)=findvarprocessed(ij)-1 - nstore=nstore+1 - if (nstore > cursnummax) then -! debug write(auditunit,*) '608: reallocate to cursnummax+incnum',curfnummax,incnum - allocate(tempstonum(cursnummax+incnum)) - tempstonum=0 - tempstonum(1:cursnummax)=stonum(1:cursnummax) - allocate(tempstofound(cursnummax+incnum)) - tempstofound=.false. - tempstofound(1:cursnummax)=stofound(1:cursnummax) - allocate(tempstovar(cursnummax+incnum)) - tempstovar=blank - tempstovar(1:cursnummax)=stovar(1:cursnummax) - - cursnummax=cursnummax+incnum - deallocate(stonum) - allocate(stonum(cursnummax)) - stonum=tempstonum - deallocate(tempstonum) - deallocate(stofound) - allocate(stofound(cursnummax)) - stofound=tempstofound - deallocate(tempstofound) - deallocate(stovar) - allocate(stovar(cursnummax)) - stovar=tempstovar - deallocate(tempstovar) - endif - if (nstore > numallowed) then - if (limited) then - nstore=numallowed - write(*,*) 'too many variables requested, will go with first ',numallowed+1 - write(auditunit,*) 'too many variables requested, will go with first ',numallowed+1 - exit - endif - endif - stonum(nstore)=j - stofound(nstore)=.true. - ! i is the position of the first comma on the line - i=i+1 - line=line(i:) - i=index(line,',') - i=i+1 - line=line(i:) - i=index(line,'!') - if (i /= 0) then - ! Without the array bounds on the LHS, the compiler warns about mismatched sizes - ! Adding them forces both LHS and RHS to be the same size - ! stovar should be blank coming into this section, so trailing blanks should persist - stovar(nstore)(1:i-1)=line(1:i-1) - stovar(nstore)=trim(stovar(nstore))//'(' - i=i+1 - line=line(i:) - ! here we're looking for the schedule, there might not be one OR - ! there might be a set of [] to note average variables. - i=index(line,'[') - if (i /= 0) then - ! averaged variable, frequency is before the [ - stovar(nstore)=trim(stovar(nstore))//line(1:i-2) - ii=index(line,']') - if (line(ii+1:ii+1) == ',') then - stovar(nstore)=trim(stovar(nstore))//line(ii+1:) - endif - stovar(nstore)=trim(stovar(nstore))//')' - else - ! regular summed variable - stovar(nstore)=trim(stovar(nstore))//line - stovar(nstore)=trim(stovar(nstore))//')' - endif - ! i=index(line,' ') - ! stovar(nstore)=trim(stovar(nstore))//line(1:i-1)//')' - endif - i=index(stovar(nstore),',') - do while (i /= 0) - stovar(nstore)(i:i)=':' - i=index(stovar(nstore),',') - enddo - exit vara - endif - enddo vara - endif - if (ik /= 0 .and. pass == 2) cycle - do ij=1,ntrack - if (j == tracknum(ij)) then - trackfound(ij)=.true. - ! i is the first comma on the line - i=i+1 - line=line(i:) - ! 2nd argument is number of items on line (after report #) in data - i=index(line,',') - i=i+1 - line=line(i:) - i=index(line,'!') - if (i /= 0) then - ! The compiler warns if you try to take part of the line and apply it to the entire trackvar - ! Adding explicit indeces seems unnecessary, but it hushes up the compiler - ! The trackvar should be blank at this point, so this should be fine - trackvar(ij)(1:i-1)=line(1:i-1) - trackvar(ij)=trim(trackvar(ij))//'(' - i=i+1 - line=line(i:) - ! here we're looking for the schedule, there might not be one OR - ! there might be a set of [] to note average variables. - i=index(line,'[') - if (i /= 0) then - ! averaged variable, frequency is before the [ - trackvar(ij)=trim(trackvar(ij))//line(1:i-2) - ii=index(line,']') - if (line(ii+1:ii+1) == ',') then - trackvar(ij)=trim(trackvar(ij))//line(ii+1:) - endif - trackvar(ij)=trim(trackvar(ij))//')' - else - ! regular summed variable - trackvar(ij)=trim(trackvar(ij))//line - trackvar(ij)=trim(trackvar(ij))//')' - endif - endif - i=index(trackvar(ij),',') - do while (i /= 0) - trackvar(ij)(i:i)=':' - i=index(trackvar(ij),',') - enddo - endif - enddo - else - ! get all the variables in the eso (requested report variables) - ! except the ones to ignore - ntrack=ntrack+1 - if (ntrack > curnummax) then -! debug write(auditunit,*) '729: reallocate to curnummax+incnum',curfnummax,incnum - allocate(temptracknum(curnummax+incnum)) - temptracknum=0 - temptracknum(1:curnummax)=tracknum(1:curnummax) - deallocate(tracknum) - allocate(temptrackfound(curnummax+incnum)) - temptrackfound=.false. - temptrackfound(1:curnummax)=trackfound(1:curnummax) - deallocate(trackfound) - allocate(temptrackvar(curnummax+incnum)) - temptrackvar=blank - temptrackvar(1:curnummax)=trackvar(1:curnummax) - deallocate(trackvar) - - curnummax=curnummax+incnum - - allocate(tracknum(curnummax)) - tracknum=temptracknum - deallocate(temptracknum) - allocate(trackfound(curnummax)) - trackfound=temptrackfound - deallocate(temptrackfound) - allocate(trackvar(curnummax)) - trackvar=temptrackvar - deallocate(temptrackvar) - endif - - if (ntrack > numallowed .and. limited) then - write(*,*) 'too many variables requested, will go with first ',numallowed - write(auditunit,*) 'too many variables requested, will go with first ',numallowed - ntrack=numallowed - read(esounit,inoutformat,end=901) line - do while (line /= 'End of Data Dictionary') - i=index(line,',') - read(line(1:i-1),*) j - if (j > maxrptnum) maxrptnum=j - read(esounit,inoutformat,end=901) line - enddo - exit - else - tracknum(ntrack)=j - ij=ntrack - trackfound(ij)=.true. - ! i is the first comma on the line - i=i+1 - ! 2nd argument is number of items on line (after report #) in data - line=line(i:) - i=index(line,',') - i=i+1 - line=line(i:) - ! Check frequency part -- after ! - i=index(line,'!') - if (i /= 0) then - ! The compiler warns if you try to take part of the line and apply it to the entire trackvar - ! Adding explicit indeces seems unnecessary, but it hushes up the compiler - ! The trackvar should be blank at this point, so this should be fine - trackvar(ij)(1:i-1)=line(1:i-1) - trackvar(ij)=trim(trackvar(ij))//'(' - i=i+1 - line=line(i:) - ! here we're looking for the schedule, there might not be one OR - ! there might be a set of [] to note average variables. - i=index(line,'[') - if (i /= 0) then - ! averaged variable, frequency is before the [ - trackvar(ij)=trim(trackvar(ij))//line(1:i-2) - ii=index(line,']') - if (line(ii+1:ii+1) == ',') then - trackvar(ij)=trim(trackvar(ij))//line(ii+1:) - endif - trackvar(ij)=trim(trackvar(ij))//')' - else - ! regular summed variable - trackvar(ij)=trim(trackvar(ij))//line - trackvar(ij)=trim(trackvar(ij))//')' - endif - endif - i=index(trackvar(ij),',') - do while (i /= 0) - trackvar(ij)(i:i)=':' - i=index(trackvar(ij),',') - enddo - endif - endif - - read(esounit,inoutformat,end=901) line - enddo - - if (pass == 2) exit - if (getvarsfromeso) exit - pass=2 - rewind(esounit) - numtoskip=6 - read(esounit,inoutformat,end=901) line - if (index(line,'Program Version') == 0) numtoskip=5 - do i=1,numtoskip - read(esounit,inoutformat,end=901) line - enddo - enddo - - ! need to sort variables, preserve input order as possible - if (ntofind > 0) then - do j=1,ntofind -! findvar has only those that were input as non-numeric -! i=index(findvar(j),',') - if (findvarnumeric(j)) cycle -! put all on that are "wildcarded" first -! if (i /= 0) cycle - if (index(findvar(j),',') == 0) cycle - do ij=1,nstore - if (stovar(ij) == blank) cycle - if (myindex(stovar(ij),trim(findvar(j))) == 0) cycle - ntrack=ntrack+1 - if (ntrack > curnummax) then -! debug write(auditunit,*) '840: reallocate to curnummax+incnum',curfnummax,incnum - allocate(temptracknum(curnummax+incnum)) - temptracknum=0 - temptracknum(1:curnummax)=tracknum(1:curnummax) - deallocate(tracknum) - allocate(temptrackfound(curnummax+incnum)) - temptrackfound=.false. - temptrackfound(1:curnummax)=trackfound(1:curnummax) - deallocate(trackfound) - allocate(temptrackvar(curnummax+incnum)) - temptrackvar=blank - temptrackvar(1:curnummax)=trackvar(1:curnummax) - deallocate(trackvar) - - curnummax=curnummax+incnum - - allocate(tracknum(curnummax)) - tracknum=temptracknum - deallocate(temptracknum) - allocate(trackfound(curnummax)) - trackfound=temptrackfound - deallocate(temptrackfound) - allocate(trackvar(curnummax)) - trackvar=temptrackvar - deallocate(temptrackvar) - endif - if (ntrack > numallowed) then - if (limited) then - write(*,*) 'too many variables requested, will go with first ',numallowed - write(auditunit,*) 'too many variables requested, will go with first ',numallowed - ntrack=numallowed - do while (line /= 'End of Data Dictionary') - read(esounit,inoutformat,end=901) line - enddo - exit - endif - endif - tracknum(ntrack)=stonum(ij) - trackvar(ntrack)=stovar(ij) - trackfound(ntrack)=stofound(ij) - stonum(ij)=0 - stovar(ij)=blank - enddo - enddo - do j=1,ntofind -! findvar has only those that were input as non-numeric - if (findvarnumeric(j)) cycle - i=index(findvar(j),',') -! now do the ones that were specified exactly -! if (i == 0) cycle - if (i > 0) findvar(j)(i:i)=':' - do ij=1,nstore - if (stovar(ij) == blank) cycle - if (myindex(stovar(ij),trim(findvar(j))) == 0) cycle - ntrack=ntrack+1 - if (ntrack > curnummax) then -! debug write(auditunit,*) '896: reallocate to curnummax+incnum',curfnummax,incnum - allocate(temptracknum(curnummax+incnum)) - temptracknum=0 - temptracknum(1:curnummax)=tracknum(1:curnummax) - deallocate(tracknum) - allocate(temptrackfound(curnummax+incnum)) - temptrackfound=.false. - temptrackfound(1:curnummax)=trackfound(1:curnummax) - deallocate(trackfound) - allocate(temptrackvar(curnummax+incnum)) - temptrackvar=blank - temptrackvar(1:curnummax)=trackvar(1:curnummax) - deallocate(trackvar) - - curnummax=curnummax+incnum - - allocate(tracknum(curnummax)) - tracknum=temptracknum - deallocate(temptracknum) - allocate(trackfound(curnummax)) - trackfound=temptrackfound - deallocate(temptrackfound) - allocate(trackvar(curnummax)) - trackvar=temptrackvar - deallocate(temptrackvar) - endif - if (ntrack > numallowed) then - if (limited) then - ntrack=numallowed - write(*,*) 'too many variables requested, will go with first ',numallowed - write(auditunit,*) 'too many variables requested, will go with first ',numallowed - exit - endif - endif - tracknum(ntrack)=stonum(ij) - trackvar(ntrack)=stovar(ij) - trackfound(ntrack)=stofound(ij) - stonum(ij)=0 - stovar(ij)=blank - enddo - enddo - endif - -!!! the number of variables to report is in variable "ntrack" -!!! write out header, then start collating the variables into columns - write(auditunit,*) ' number variables requested for output=',ntrack - - if (.not. limited) then - if (ntrack > 3500) then - write(*,*) 'potentially too many variables requested. program may crash.' - write(*,*) ' number requested=',ntrack - write(*,*) ' program has been tested through max=3500' - write(auditunit,*) 'potentially too many variables requested. program may crash.' - write(auditunit,*) ' number requested=',ntrack - write(auditunit,*) ' program has been tested through max=3500' - endif - endif - - allocate(outdata(ntrack)) - outdata=blank - allocate(outfound(ntrack)) - outfound=.false. - -!!! -!!! Write out "header line" -!!! - - outlinepart='Date/Time' - write(csvunit,inoutformat,ADVANCE='NO') trim(outlinepart) - do ij=1,ntrack - if (trackfound(ij)) then - outlinepart=sepvar//trim(trackvar(ij)) - write(csvunit,inoutformat,ADVANCE='NO') trim(outlinepart) - else - write(*,*) 'line 904 variable =',tracknum(ij),' not found' - write(auditunit,*) 'line 904 variable =',tracknum(ij),' not found' - endif - enddo - if (FixHeader) then - write(csvunit,inoutformat) ! final..trim(outline) - else - write(csvunit,inoutformat) ' ' - endif - read(esounit,inoutformat) line ! first environment line - curdate=blank - curmonday=blank - curmon=blank - curper=blank - - allocate(trackindex(maxrptnum)) - trackindex=0 - do j = 1, ntrack - trackIndex(tracknum(j))=j -! debug write(auditunit,'(1X,I5,1X,I5,1X,A)') tracknum(j),trackIndex(tracknum(j)),trim(trackvar(j)) - end do - -!!! Collate data into columns. - - outline=blank - -100 continue - read(esounit,inoutformat,end=902) line - if (line == 'End of Data') then - ! there are some to write - useoutline=.false. - if (.not. nodetails) then - outline=trim(curdate) - elseif (.not. nomonday) then - outline=trim(curmonday) - elseif (.not. nomon) then - outline=trim(curmon) - else - outline=trim(curper) - endif - anytoprint=.false. -! debug write(auditunit,*) '1008: end of data line, writing some=',trim(outline) - commacount=0 - do j=1,ntrack - if (outfound(j)) then - if (.not. anytoprint) then - write(csvunit,inoutformat,advance='no') trim(outline) ! writing out the period line - outline=blank - commacount=0 - endif - if (useoutline) then - write(csvunit,inoutformat,advance='no') trim(outline) - useoutline=.false. - outline=blank - commacount=0 - endif - write(csvunit,inoutformat,advance='no') sepvar//trim(outdata(j)) - anytoprint=.true. - else - outline=trim(outline)//sepvar - useoutline=.true. - commacount=commacount+1 - if (commacount > commalimit) then - write(csvunit,inoutformat,advance='no') trim(outline) - outline=blank - useoutline=.false. - commacount=0 - endif - endif - enddo - if (anytoprint) then - write(csvunit,inoutformat) ' ' - outline=blank - endif - outfound=.false. - goto 900 - endif - if (line == blank) goto 907 - i=index(line,',') - if (i == 0) goto 906 - read(line(1:i-1),*,err=906) lineno - select case (lineno) - - case (1) ! Environment line - - case (2) ! Date/Hour line - nodetails=.false. - read(line,*,err=906) iform,dayofsim,nmonth,nday,dstind,nhourofday,nsminute,neminute -! write or not? - commacount=0 - if (curdate /= blank) then - if (nhourofday .ne. hourofday .or. neminute .ne. eminute) then - ! there are some to write - outline=trim(curdate) - anytoprint=.false. - useoutline=.false. - do j=1,ntrack - if (outfound(j)) then - if (.not. anytoprint) then - write(csvunit,inoutformat,advance='no') trim(outline) ! writing out the period line - outline=blank - commacount=0 - endif - if (useoutline) then - write(csvunit,inoutformat,advance='no') trim(outline) - useoutline=.false. - outline=blank - commacount=0 - endif - write(csvunit,inoutformat,advance='no') sepvar//trim(outdata(j)) - anytoprint=.true. - else - outline=trim(outline)//sepvar - useoutline=.true. - commacount=commacount+1 - if (commacount > commalimit) then - write(csvunit,inoutformat,advance='no') trim(outline) - outline=blank - useoutline=.false. - commacount=0 - endif - endif - enddo - if (anytoprint) then - write(csvunit,inoutformat) ' ' - outline=blank - endif - outfound=.false. - endif - endif - eminute=neminute - sminute=nsminute - day=nday - month=nmonth - hourofday=nhourofday - curhr=hourofday-1 - curmin=int(eminute) - cursec=(eminute-curmin)*60. - if (eminute == 60.0) then - curhr=hourofday - curmin=0 - curdate=blank - write(curdate,dateformat) month,day,curhr,curmin,cursec - if (sminute == 0.0) then - write(curdate,dateformat) month,day,curhr,curmin,cursec - endif - else - write(curdate,dateformat) month,day,curhr,curmin,cursec - endif - - case(3) ! Month/day (daily) - if (nodetails) then - nomonday=.false. - read(line,*,err=906) iform,dayofsim,nmonth,nday,dstind - commacount=0 - if (curmonday /= blank) then - ! there are some to write - outline=trim(curmonday) - anytoprint=.false. - useoutline=.false. - do j=1,ntrack - if (outfound(j)) then - if (.not. anytoprint) then - write(csvunit,inoutformat,advance='no') trim(outline) ! writing out the period line - outline=blank - commacount=0 - endif - if (useoutline) then - write(csvunit,inoutformat,advance='no') trim(outline) - useoutline=.false. - outline=blank - commacount=0 - endif - write(csvunit,inoutformat,advance='no') sepvar//trim(outdata(j)) - anytoprint=.true. - else - outline=trim(outline)//sepvar - useoutline=.true. - commacount=commacount+1 - if (commacount > commalimit) then - write(csvunit,inoutformat,advance='no') trim(outline) - outline=blank - useoutline=.false. - commacount=0 - endif - endif - enddo - if (anytoprint) then - write(csvunit,inoutformat) ' ' - outline=blank - endif - outfound=.false. - endif - day=nday - month=nmonth - write(curmonday,dateformat) month,day - endif - - case(4) ! month (monthly) -! debug write(auditunit,*) '1131: at monthly' - if (nodetails .and. nomonday) then - nomon=.false. - read(line,*,err=906) iform,dayofsim,nmonth - commacount=0 - if (curmon /= blank) then - ! there are some to write - outline=trim(curmon) -! debug write(auditunit,*) '1141: writing some=',trim(curmon) - anytoprint=.false. - useoutline=.false. - do j=1,ntrack - if (outfound(j)) then - if (.not. anytoprint) then - write(csvunit,inoutformat,advance='no') trim(outline) ! writing out the period line - outline=blank - commacount=0 - endif - if (useoutline) then - write(csvunit,inoutformat,advance='no') trim(outline) - useoutline=.false. - outline=blank -! debug write(auditunit,*) '1158: useoutline, commacount=',commacount - commacount=0 - endif - write(csvunit,inoutformat,advance='no') sepvar//trim(outdata(j)) - anytoprint=.true. - else - outline=trim(outline)//sepvar - useoutline=.true. - commacount=commacount+1 - if (commacount > commalimit) then - write(csvunit,inoutformat,advance='no') trim(outline) - outline=blank - useoutline=.false. - commacount=0 - endif - endif - enddo -! debug write(auditunit,*) 'commacount=',commacount - if (anytoprint) then - write(csvunit,inoutformat) ' ' - outline=blank - endif - outfound=.false. - endif - month=nmonth - write(curmon,702) months(month) - curmonlen=monlen(month) - 702 format(a) - endif - - case(5) ! period -! debug write(auditunit,*) '1171: at runperiod' - if (nodetails .and. nomonday .and. nomon) then - read(line,*,err=906) iform,dayofsim - commacount=0 - if (curper /= blank) then - ! there are some to write - outline=trim(curper) - anytoprint=.false. - useoutline=.false. -! debug write(auditunit,*) '1184: writing some=',trim(curper) - do j=1,ntrack - if (outfound(j)) then - if (.not. anytoprint) then - write(csvunit,inoutformat,advance='no') trim(outline) ! writing out the period line - outline=blank - commacount=0 - endif - if (useoutline) then - write(csvunit,inoutformat,advance='no') trim(outline) - useoutline=.false. - outline=blank - commacount=0 - endif - write(csvunit,inoutformat,advance='no') sepvar//trim(outdata(j)) - anytoprint=.true. - else - outline=trim(outline)//sepvar - useoutline=.true. - commacount=commacount+1 - if (commacount > commalimit) then - write(csvunit,inoutformat,advance='no') trim(outline) - outline=blank - useoutline=.false. - commacount=0 - endif - endif - enddo - if (anytoprint) then - write(csvunit,inoutformat) ' ' - outline=blank - endif - outfound=.false. - endif - write(cdayofsim,*) dayofsim - cdayofsim=adjustl(cdayofsim) - write(curper,701) trim(cdayofsim) - 701 format('simdays=',A) - endif - - case default ! all the rest are data lines -! if (lineno > size(trackindex)) goto 100 - j = trackIndex(lineno) - if (j /= 0) then - ! i currently positioned at , -- , - i=i+1 - line=line(i:) - i=index(line,',') ! position i after value on line -- ,, - if (i == 0) then - outdata(j)=line - else - outdata(j)=line(1:i-1) - endif -! outdata(j)=adjustl(outdata(j)) - outfound(j)=.true. - endif - end select - goto 100 - -900 close(csvunit) - close(esounit) - CALL CPU_TIME(Time_Finish) - Elapsed_Time=Time_Finish-Time_Start - do ij=1,ntoignore - if (ij == 1) write(auditunit,*) 'ignoring:' - write(auditunit,'(a)') trim(ignorefindvar(ij)) - enddo - do ij=1,ntofind - if (ij == 1) write(auditunit,*) 'found/finding:' - if (findvarprocessed(ij) >= 0) then - write(auditunit,'(i6,1x,a)') findvarprocessed(ij),trim(findvar(ij)) - else ! wildcarded - write(auditunit,'(i6,1x,a)') abs(findvarprocessed(ij)),trim(findvar(ij))//'*' - endif - enddo - - Hours=Elapsed_Time/3600. - Elapsed_Time=Elapsed_Time-Hours*3600 - Minutes=Elapsed_Time/60. - Elapsed_Time=Elapsed_Time-Minutes*60 - Seconds=Elapsed_Time - WRITE(Elapsed,"(I2.2,'hr ',I2.2,'min ',F5.2,'sec')") Hours,Minutes,Seconds - CALL DisplayString('ReadVars Run Time='//TRIM(Elapsed)) - write(auditunit,*) 'ReadVars Run Time='//TRIM(Elapsed) - if (.not. ErrorsHappened) then - write(*,*) 'ReadVarsESO program completed successfully.' - write(auditunit,*) 'ReadVarsESO program completed successfully.' - endif - - close(auditunit) - stop - -901 write(*,*) 'EOF encountered during read of ESO header records' - write(*,*) 'probable EnergyPlus error condition -- check eplusout.err' - write(*,*) 'ReadVarsESO program terminated.' - write(auditunit,*) 'EOF encountered during read of ESO header records' - write(auditunit,*) 'probable EnergyPlus error condition -- check eplusout.err' - write(auditunit,*) 'ReadVarsESO program terminated.' - ErrorsHappened=.true. - goto 900 - -902 write(*,*) 'EOF encountered on eplusout.eso while reading data' - write(*,*) 'probable EnergyPlus error condition -- check eplusout.err' - write(*,*) 'ReadVarsESO program terminated.' - write(auditunit,*) 'EOF encountered on eplusout.eso while reading data' - write(auditunit,*) 'probable EnergyPlus error condition -- check eplusout.err' - write(auditunit,*) 'ReadVarsESO program terminated.' - ErrorsHappened=.true. - goto 900 - -903 write(*,*) 'Requested ESO file='//trim(inputfilename) - write(*,*) 'does not exist. ReadVarsESO program terminated.' - write(*,*) 'ReadVarsESO program terminated.' - write(auditunit,*) 'Requested ESO file='//trim(inputfilename) - write(auditunit,*) 'does not exist. ReadVarsESO program terminated.' - write(auditunit,*) 'ReadVarsESO program terminated.' - stop - -904 write(*,*) 'Requested Report Variable input file='//trim(varfilename) - write(*,*) 'does not exist. Check eplusout.err file for possible explanations.' - write(*,*) 'ReadVarsESO program terminated.' - write(auditunit,*) 'Requested Report Variable input file='//trim(varfilename) - write(auditunit,*) 'does not exist. Check eplusout.err file for possible explanations.' - write(auditunit,*) 'ReadVarsESO program terminated.' - stop - -905 write(*,*) 'Output file='//trim(outputfilename) - write(*,*) 'cannot be opened. It may be open in another program.' - write(*,*) 'Please close it and try again.' - write(*,*) 'ReadVarsESO program terminated.' - write(auditunit,*) 'Output file='//trim(outputfilename) - write(auditunit,*) 'cannot be opened. It may be open in another program.' - write(auditunit,*) 'Please close it and try again.' - write(auditunit,*) 'ReadVarsESO program terminated.' - stop - -906 write(*,*) 'Output file='//trim(outputfilename) - write(*,*) 'error occurred during processing.' - write(*,*) 'Apparent line in error (1st 50 characters):' - write(*,*) trim(line(1:50)) - write(*,*) 'ReadVarsESO program terminated.' - write(auditunit,*) 'Output file='//trim(outputfilename) - write(auditunit,*) 'error occurred during processing.' - write(auditunit,*) 'Apparent line in error (1st 50 characters):' - write(auditunit,*) trim(line(1:50)) - write(auditunit,*) 'ReadVarsESO program terminated.' - stop - -907 write(*,*) 'Output file='//trim(outputfilename) - write(*,*) 'error occurred during processing.' - write(*,*) 'Blank line in middle of processing.' - write(*,*) 'Likely fatal error during EnergyPlus execution.' - write(*,*) 'ReadVarsESO program terminated.' - write(auditunit,*) 'Output file='//trim(outputfilename) - write(auditunit,*) 'error occurred during processing.' - write(auditunit,*) 'Blank line in middle of processing.' - write(auditunit,*) 'Likely fatal error during EnergyPlus execution.' - write(auditunit,*) 'ReadVarsESO program terminated.' - stop - -contains - -REAL FUNCTION ProcessNumber(String) - - ! FUNCTION INFORMATION: - ! AUTHOR Linda K. Lawrie - ! DATE WRITTEN September 1997 - ! MODIFIED na - ! RE-ENGINEERED na - - ! PURPOSE OF THIS FUNCTION: - ! This function processes a string that should be numeric and - ! returns the real value of the string. - - ! METHODOLOGY EMPLOYED: - ! FUNCTION ProcessNumber translates the argument (a string) - ! into a real number. The string should consist of all - ! numeric characters (except a decimal point). Numerics - ! with exponentiation (i.e. 1.2345E+03) are allowed but if - ! it is not a valid number an error message along with the - ! string causing the error is printed out and 0.0 is returned - ! as the value. - - ! The Fortran input processor is used to make the conversion. - - ! REFERENCES: - ! List directed Fortran input/output. - - ! USE STATEMENTS: - ! na - - IMPLICIT NONE ! Enforce explicit typing of all variables in this routine - - ! SUBROUTINE ARGUMENT DEFINITIONS: - CHARACTER(len=*), INTENT(IN) :: String - - ! SUBROUTINE PARAMETER DEFINITIONS: - CHARACTER(len=*), PARAMETER :: ValidNumerics='0123456789.+-'//CHAR(9) - - ! INTERFACE BLOCK SPECIFICATIONS - ! na - - ! DERIVED TYPE DEFINITIONS - ! na - - ! SUBROUTINE LOCAL VARIABLE DECLARATIONS: - - REAL Temp - INTEGER IoStatus - INTEGER Number - - - ProcessNumber=0.0 - ! See if first character is a valid numeric - Number=SCAN(String(1:1),ValidNumerics) - IF (Number /= 0) THEN - Read(String,*,IOSTAT=IoStatus) Temp - ELSE - IoStatus=-999 - ENDIF - IF (IoStatus == 0) THEN - Read(String,*,IOSTAT=IoStatus) Temp - ENDIF - IF (IoStatus == 0) THEN - ProcessNumber=Temp - ELSE - IF (String == ' ') THEN - ProcessNumber=-999 - ELSE - !CALL ShowSevereError('Invalid Number in Input='//TRIM(String),EchoInputFile) - ProcessNumber=-999 - ENDIF - ENDIF - -RETURN - -END FUNCTION ProcessNumber - -FUNCTION MakeUPPERCase(InputString) RESULT (ResultString) - - ! FUNCTION INFORMATION: - ! AUTHOR Linda K. Lawrie - ! DATE WRITTEN September 1997 - ! MODIFIED na - ! RE-ENGINEERED na - - ! PURPOSE OF THIS SUBROUTINE: - ! This function returns the Upper Case representation of the InputString. - - ! METHODOLOGY EMPLOYED: - ! There is method to this madness. - - ! REFERENCES: - ! na - - ! USE STATEMENTS: - ! na - - IMPLICIT NONE ! Enforce explicit typing of all variables in this routine - - - ! FUNCTION ARGUMENT DEFINITIONS: - CHARACTER(len=*), INTENT(IN) :: InputString - CHARACTER(len=len(InputString)) ResultString - - - ! FUNCTION PARAMETER DEFINITIONS: - CHARACTER(len=26), PARAMETER :: UpperCase='ABCDEFGHIJKLMNOPQRSTUVWXYZ' - CHARACTER(len=26), PARAMETER :: LowerCase='abcdefghijklmnopqrstuvwxyz' - - ! INTERFACE BLOCK SPECIFICATIONS - ! na - - ! DERIVED TYPE DEFINITIONS - ! na - - ! FUNCTION LOCAL VARIABLE DECLARATIONS: - INTEGER Count - INTEGER Pos - INTEGER LengthInputString - - ResultString=' ' - LengthInputString=LEN_TRIM(InputString) - DO Count=1,LengthInputString - Pos=SCAN(LowerCase,InputString(Count:Count)) - IF (Pos /= 0) THEN - ResultString(Count:Count)=UpperCase(Pos:Pos) - ELSE - ResultString(Count:Count)=InputString(Count:Count) - ENDIF - END DO - ResultString=TRIM(ResultString) - - RETURN - -END FUNCTION MakeUPPERCase - -INTEGER FUNCTION myindex(String,SubString) - -Character(len=*),INTENT(IN) :: String -Character(len=*),INTENT(IN) :: SubString - -myindex=index(MakeUPPERCase(String),MakeUPPERCase(SubString)) - -END FUNCTION myindex - - SUBROUTINE DisplayString(String) - Implicit NONE - character(len=*) String -! -!d title:= DisplayString - display string in program -!d purpose:= This routine provides a call to display strings -!d during program execution. -!d usage:= -!d call DisplayString(string) -!d implementation dependencies:= -!d none. -!d variable dictionary:= -!d string - argument - string to be displayed. -! - write(*,*) trim(String) - RETURN - END SUBROUTINE DisplayString - -end program readvarseso diff --git a/src/ReadVars/ReadVarsESO.in b/src/ReadVars/ReadVarsESO.in new file mode 100644 index 00000000000..155597fd8aa --- /dev/null +++ b/src/ReadVars/ReadVarsESO.in @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +script_dir=$(dirname "$0") +if command -v python3 >/dev/null 2>&1; then + exec python3 "$script_dir/ReadVarsESO.py" "$@" +fi +exec python "$script_dir/ReadVarsESO.py" "$@" diff --git a/src/ReadVars/ReadVarsESO.py b/src/ReadVars/ReadVarsESO.py new file mode 100644 index 00000000000..d87764cd7eb --- /dev/null +++ b/src/ReadVars/ReadVarsESO.py @@ -0,0 +1,883 @@ +#!/usr/bin/env python3 +"""Read EnergyPlus ESO/MTR output files and write delimited tables. + +This is a Python port of the historical ReadVarsESO Fortran utility. It keeps +the same command-line interface: an optional RVI/MVI file, optional frequency +filter, optional ``unlimited``/``nolimit`` column handling, and optional +``fixheader`` output. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +import sys +import time +from typing import Iterable, TextIO + + +NUM_ALLOWED = 255 +UNLIMITED_WARNING_COUNT = 3500 +MAX_OUTPUT_VALUE_LENGTH = 25 + +FREQUENCY_MARKERS = { + 1: "!TimeStep", + 2: "!Hourly", + 3: "!Daily", + 4: "!Monthly", + 5: "!RunPeriod", +} + +MONTHS = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +] + + +@dataclass +class Options: + var_file_name: str + get_vars_from_eso: bool + frequency: int + limited: bool + fix_header: bool + + +@dataclass +class Requests: + track_numbers: list[int] + ignore_numbers: list[int] + find_variables: list[str] + find_variable_processed: list[int] + ignore_find_variables: list[str] + + +@dataclass +class DictionaryRecord: + number: int + line: str + label: str + + +@dataclass +class SelectedVariable: + number: int + label: str + found: bool + + +class ReadVarsFatal(Exception): + def __init__(self, messages: Iterable[str], exit_code: int = 1): + self.messages = list(messages) + self.exit_code = exit_code + super().__init__("\n".join(self.messages)) + + +class EarlyExit(Exception): + pass + + +def display_string(message: str) -> None: + print(message) + + +def audit_write(audit: TextIO | None, message: str = "") -> None: + if audit is not None: + audit.write(f"{message}\n") + + +def fatal(audit: TextIO | None, messages: Iterable[str], exit_code: int = 1) -> None: + materialized = list(messages) + for message in materialized: + display_string(message) + audit_write(audit, message) + raise ReadVarsFatal([], exit_code) + + +def parse_options(argv: list[str]) -> Options: + if not argv: + return Options("", True, 0, True, False) + + var_file_name = argv[0].lstrip() + get_vars_from_eso = var_file_name == "" + frequency = 0 + limited = True + fix_header = False + + for raw_arg in argv[1:]: + arg = raw_arg.strip().lower() + if arg.startswith("t") or arg.startswith("de"): + frequency = 1 + if arg.startswith("h"): + frequency = 2 + if arg.startswith("da"): + frequency = 3 + if arg.startswith("m"): + frequency = 4 + if arg.startswith("a") or arg.startswith("r"): + frequency = 5 + if arg.startswith("u") or arg.startswith("n"): + limited = False + if arg.startswith("f"): + fix_header = True + + return Options(var_file_name, get_vars_from_eso, frequency, limited, fix_header) + + +def read_lines(path: Path) -> list[str]: + with path.open("r", encoding="utf-8-sig", errors="replace") as handle: + return [line.rstrip("\r\n") for line in handle] + + +def strip_comment_for_file_name(line: str, audit: TextIO | None) -> str | None: + line = line.lstrip() + if line.startswith("!"): + audit_write(audit, f" ignoring comment line={line.rstrip()}") + return None + + comment_position = line.find("!") + if comment_position > 0: + audit_write(audit, f"comment stripped on line:{line.rstrip()}") + line = line[:comment_position] + elif comment_position == 0: + line = "" + + return line.strip() + + +def separator_for_output(output_file_name: str) -> str: + suffix = Path(output_file_name.strip()).suffix.lower() + if suffix == ".tab": + return "\t" + if suffix == ".txt": + return " " + return "," + + +def process_number(text: str) -> float: + valid_first = "0123456789.+-\t" + if not text or text[0] not in valid_first: + return -999.0 + + stripped = text.strip() + if not stripped: + return -999.0 + + token = stripped.split()[0] + try: + return float(token) + except ValueError: + return -999.0 + + +def normalize_variable_request(line: str) -> str: + bracket_position = line.find("[") + if bracket_position != -1: + line = line[:bracket_position] + + temp_var = line.lstrip() + comma_position = temp_var.find(",") + if comma_position != -1: + return temp_var[: comma_position + 1].rstrip() + temp_var[comma_position + 1 :].lstrip() + + return temp_var.strip() + + +def parse_rvi_variable_requests(lines: list[str], audit: TextIO | None) -> Requests: + requests = Requests([], [], [], [], []) + done = False + + for raw_line in lines: + if done: + break + + line = raw_line.replace("\t", " ").lstrip() + comment_position = line.find("!") + if comment_position > 0: + audit_write(audit, f" stripping comment from line={line.rstrip()}") + line = line[:comment_position] + elif comment_position == 0: + audit_write(audit, f" ignoring comment line={line.rstrip()}") + continue + + ignore_this_one = False + if "," not in line: + if line.startswith("~"): + ignore_this_one = True + number = process_number(line[1:]) + else: + number = process_number(line) + else: + number = -999.0 + + if line.strip() == "": + number = 0.0 + + if number > 0: + report_number = int(number) + if ignore_this_one: + requests.ignore_numbers.append(report_number) + else: + requests.track_numbers.append(report_number) + continue + + if number < 0: + if line.startswith("~"): + request = normalize_variable_request(line[1:]) + requests.ignore_find_variables.append(request) + else: + request = normalize_variable_request(line) + requests.find_variables.append(request) + requests.find_variable_processed.append(0) + else: + done = True + + return requests + + +def read_rvi_configuration(options: Options, audit: TextIO | None) -> tuple[str, str, str, bool, list[str]]: + if options.get_vars_from_eso: + input_file_name = "eplusout.eso" + if not Path(input_file_name).is_file(): + fatal( + audit, + [ + f"Requested ESO file={input_file_name}", + "does not exist. ReadVarsESO program terminated.", + "ReadVarsESO program terminated.", + ], + ) + return input_file_name, "eplusout.csv", ",", True, [] + + var_file = Path(options.var_file_name) + if not var_file.is_file(): + fatal( + audit, + [ + f"Requested Report Variable input file={options.var_file_name}", + "does not exist. Check eplusout.err file for possible explanations.", + "ReadVarsESO program terminated.", + ], + ) + + audit_write(audit, f"processing:{options.var_file_name}") + rvi_lines = read_lines(var_file) + cursor = 0 + + input_file_name = "" + while cursor < len(rvi_lines) and not input_file_name: + parsed = strip_comment_for_file_name(rvi_lines[cursor], audit) + cursor += 1 + if parsed is None: + continue + input_file_name = parsed or "eplusout.eso" + + if not input_file_name: + input_file_name = "eplusout.eso" + output_file_name = "eplusout.csv" + if not Path(input_file_name).is_file(): + fatal( + audit, + [ + f"Requested ESO file={input_file_name}", + "does not exist. ReadVarsESO program terminated.", + "ReadVarsESO program terminated.", + ], + ) + return input_file_name, output_file_name, ",", True, [] + + if not Path(input_file_name).is_file(): + fatal( + audit, + [ + f"Requested ESO file={input_file_name}", + "does not exist. ReadVarsESO program terminated.", + "ReadVarsESO program terminated.", + ], + ) + + audit_write(audit, f"input file:{input_file_name}") + + output_file_name = "" + while cursor < len(rvi_lines) and not output_file_name: + parsed = strip_comment_for_file_name(rvi_lines[cursor], audit) + cursor += 1 + if parsed is None: + continue + output_file_name = parsed or "eplusout.csv" + + if not output_file_name: + output_file_name = "eplusout.csv" + + separator = separator_for_output(output_file_name) + + remaining_lines = rvi_lines[cursor:] + get_vars_from_eso = False + if not remaining_lines: + get_vars_from_eso = True + else: + probe = remaining_lines[0].lstrip() + if probe.strip() == "" or probe.strip() == "0": + get_vars_from_eso = True + remaining_lines = remaining_lines[1:] + + return input_file_name, output_file_name, separator, get_vars_from_eso, remaining_lines + + +def parse_report_number(line: str) -> int | None: + comma_position = line.find(",") + if comma_position == -1: + return None + + try: + return int(float(line[:comma_position].strip())) + except ValueError: + return None + + +def build_header_label(line: str) -> str: + fields = line.split(",", 2) + if len(fields) < 3: + return "" + + remainder = fields[2] + bang_position = remainder.find("!") + if bang_position == -1: + return remainder.strip().replace(",", ":") + + label = remainder[:bang_position].rstrip() + "(" + frequency_part = remainder[bang_position + 1 :].rstrip() + bracket_position = frequency_part.find("[") + + if bracket_position != -1: + label += frequency_part[: max(bracket_position - 1, 0)] + close_bracket_position = frequency_part.find("]", bracket_position + 1) + if close_bracket_position != -1: + next_position = close_bracket_position + 1 + if next_position < len(frequency_part) and frequency_part[next_position] == ",": + label += frequency_part[next_position:] + label += ")" + else: + label += frequency_part + ")" + + return label.replace(",", ":") + + +def dictionary_records(eso_lines: list[str], audit: TextIO | None) -> tuple[list[DictionaryRecord], int]: + end_index = None + for index, line in enumerate(eso_lines): + if line.strip() == "End of Data Dictionary": + end_index = index + break + + if end_index is None: + fatal( + audit, + [ + "EOF encountered during read of ESO header records", + "probable EnergyPlus error condition -- check eplusout.err", + "ReadVarsESO program terminated.", + ], + ) + + records: list[DictionaryRecord] = [] + for line in eso_lines[:end_index]: + number = parse_report_number(line) + if number is None or number <= 5: + continue + records.append(DictionaryRecord(number, line, build_header_label(line))) + + return records, end_index + + +def is_allowed_frequency(line: str, frequency: int) -> bool: + marker = FREQUENCY_MARKERS.get(frequency) + return marker is None or marker in line + + +def myindex(text: str, substring: str) -> int: + if not substring: + return -1 + return text.upper().find(substring.upper()) + + +def is_ignored(record: DictionaryRecord, requests: Requests) -> bool: + if record.number in requests.ignore_numbers: + return True + + for ignore in requests.ignore_find_variables: + if myindex(record.line, ignore.strip()) != -1: + return True + + return False + + +def request_matches_dictionary_line(line: str, request: str, exact: bool) -> bool: + request = request.strip() + position = myindex(line, request) + if position == -1: + return False + + if position == 0 or line[position - 1] != ",": + return False + + if not exact: + return True + + suffix = line[position:] + bracket_position = suffix.find("[") + bang_position = suffix.find("!") + if bracket_position != -1: + end_position = bracket_position + elif bang_position != -1: + end_position = bang_position + else: + end_position = len(suffix) + + return suffix[:end_position].rstrip().upper() == request.upper() + + +def warn_too_many_variables(audit: TextIO | None, limit: int = NUM_ALLOWED) -> None: + message = f"too many variables requested, will go with first {limit}" + display_string(message) + audit_write(audit, message) + + +def selected_with_limit( + selected: list[SelectedVariable], + limited: bool, + audit: TextIO | None, +) -> list[SelectedVariable]: + if limited and len(selected) > NUM_ALLOWED: + warn_too_many_variables(audit) + return selected[:NUM_ALLOWED] + + if not limited and len(selected) > UNLIMITED_WARNING_COUNT: + messages = [ + "potentially too many variables requested. program may crash.", + f" number requested={len(selected)}", + f" program has been tested through max={UNLIMITED_WARNING_COUNT}", + ] + for message in messages: + display_string(message) + audit_write(audit, message) + + return selected + + +def stage_named_variables( + records: list[DictionaryRecord], + requests: Requests, + numeric_track_numbers: set[int], +) -> list[DictionaryRecord]: + staged: list[DictionaryRecord] = [] + staged_numbers: set[int] = set() + + for exact in (True, False): + for record in records: + for index, request in enumerate(requests.find_variables): + if not request: + continue + if not exact and requests.find_variable_processed[index] > 0: + continue + if not request_matches_dictionary_line(record.line, request, exact): + continue + + if exact: + requests.find_variable_processed[index] += 1 + + if record.number in numeric_track_numbers or record.number in staged_numbers: + break + + if not exact: + requests.find_variable_processed[index] -= 1 + + staged.append(record) + staged_numbers.add(record.number) + break + + return staged + + +def append_selected( + selected: list[SelectedVariable], + record: DictionaryRecord, +) -> None: + selected.append(SelectedVariable(record.number, record.label, True)) + + +def select_variables( + records: list[DictionaryRecord], + requests: Requests, + get_vars_from_eso: bool, + frequency: int, + limited: bool, + audit: TextIO | None, +) -> list[SelectedVariable]: + allowed_records = [ + record + for record in records + if is_allowed_frequency(record.line, frequency) and not is_ignored(record, requests) + ] + + if get_vars_from_eso: + selected = [SelectedVariable(record.number, record.label, True) for record in allowed_records] + return selected_with_limit(selected, limited, audit) + + selected = [SelectedVariable(number, "", False) for number in requests.track_numbers] + + for record in allowed_records: + for variable in selected: + if variable.number == record.number: + variable.label = record.label + variable.found = True + + staged = stage_named_variables(allowed_records, requests, set(requests.track_numbers)) + remaining = staged[:] + + for request in requests.find_variables: + if "," not in request: + continue + for record in remaining[:]: + if myindex(record.label, request.strip()) != -1: + append_selected(selected, record) + remaining.remove(record) + + for request in requests.find_variables: + comparable_request = request.strip() + comma_position = comparable_request.find(",") + if comma_position != -1: + comparable_request = comparable_request[:comma_position] + ":" + comparable_request[comma_position + 1 :] + + for record in remaining[:]: + if myindex(record.label, comparable_request) != -1: + append_selected(selected, record) + remaining.remove(record) + + return selected_with_limit(selected, limited, audit) + + +def write_header(output: TextIO, selected: list[SelectedVariable], separator: str, fix_header: bool, audit: TextIO | None) -> None: + header_parts = ["Date/Time"] + for variable in selected: + if variable.found: + header_parts.append(variable.label.strip()) + else: + message = f"line 904 variable ={variable.number} not found" + display_string(message) + audit_write(audit, message) + + output.write(separator.join(header_parts)) + output.write("\n" if fix_header else " \n") + + +def split_numeric_fields(line: str, count: int) -> list[float]: + fields = line.split(",") + if len(fields) < count: + raise ValueError + return [float(fields[index].strip()) for index in range(count)] + + +def format_time_stamp(month: int, day: int, hour_of_day: int, start_minute: float, end_minute: float) -> str: + current_hour = hour_of_day - 1 + current_minute = int(end_minute) + current_second = int((end_minute - current_minute) * 60.0) + if end_minute == 60.0: + current_hour = hour_of_day + current_minute = 0 + if start_minute == 0.0: + current_second = 0 + return f" {month:02d}/{day:02d} {current_hour:02d}:{current_minute:02d}:{current_second:02d}" + + +def format_month_day(month: int, day: int) -> str: + return f" {month:02d}/{day:02d}" + + +def flush_row( + output: TextIO, + label: str, + out_data: list[str], + out_found: list[bool], + separator: str, +) -> None: + if not any(out_found): + return + + row_parts = [label.rstrip()] + for index, found in enumerate(out_found): + row_parts.append(out_data[index].rstrip() if found else "") + output.write(separator.join(row_parts)) + output.write(" \n") + + for index in range(len(out_data)): + out_data[index] = "" + out_found[index] = False + + +def process_data_records( + eso_lines: list[str], + data_start_index: int, + selected: list[SelectedVariable], + output: TextIO, + output_file_name: str, + separator: str, + audit: TextIO | None, +) -> None: + out_data = [""] * len(selected) + out_found = [False] * len(selected) + track_index = {variable.number: index for index, variable in enumerate(selected)} + + no_details = True + no_month_day = True + no_month = True + current_date = "" + current_month_day = "" + current_month = "" + current_period = "" + previous_hour_of_day: int | None = None + previous_end_minute: float | None = None + + for line in eso_lines[data_start_index:]: + if line.strip() == "End of Data": + if not no_details: + label = current_date + elif not no_month_day: + label = current_month_day + elif not no_month: + label = current_month + else: + label = current_period + flush_row(output, label, out_data, out_found, separator) + return + + if line.strip() == "": + fatal( + audit, + [ + f"Output file={output_file_name}", + "error occurred during processing.", + "Blank line in middle of processing.", + "Likely fatal error during EnergyPlus execution.", + "ReadVarsESO program terminated.", + ], + ) + + comma_position = line.find(",") + if comma_position == -1: + processing_error(output_file_name, line, audit) + + try: + line_number = int(float(line[:comma_position].strip())) + except ValueError: + processing_error(output_file_name, line, audit) + + if line_number == 1: + continue + + if line_number == 2: + no_details = False + try: + fields = split_numeric_fields(line, 8) + except ValueError: + processing_error(output_file_name, line, audit) + + month = int(fields[2]) + day = int(fields[3]) + hour_of_day = int(fields[5]) + start_minute = fields[6] + end_minute = fields[7] + + if current_date and (hour_of_day != previous_hour_of_day or end_minute != previous_end_minute): + flush_row(output, current_date, out_data, out_found, separator) + + previous_hour_of_day = hour_of_day + previous_end_minute = end_minute + current_date = format_time_stamp(month, day, hour_of_day, start_minute, end_minute) + continue + + if line_number == 3: + if no_details: + no_month_day = False + try: + fields = split_numeric_fields(line, 5) + except ValueError: + processing_error(output_file_name, line, audit) + + if current_month_day: + flush_row(output, current_month_day, out_data, out_found, separator) + + month = int(fields[2]) + day = int(fields[3]) + current_month_day = format_month_day(month, day) + continue + + if line_number == 4: + if no_details and no_month_day: + no_month = False + try: + fields = split_numeric_fields(line, 3) + except ValueError: + processing_error(output_file_name, line, audit) + + if current_month: + flush_row(output, current_month, out_data, out_found, separator) + + month_index = int(fields[2]) - 1 + if month_index < 0 or month_index >= len(MONTHS): + processing_error(output_file_name, line, audit) + current_month = MONTHS[month_index] + continue + + if line_number == 5: + if no_details and no_month_day and no_month: + try: + fields = split_numeric_fields(line, 2) + except ValueError: + processing_error(output_file_name, line, audit) + + if current_period: + flush_row(output, current_period, out_data, out_found, separator) + + current_period = f"simdays={int(fields[1])}" + continue + + selected_index = track_index.get(line_number) + if selected_index is None: + continue + + value_start = comma_position + 1 + value_remainder = line[value_start:] + next_comma_position = value_remainder.find(",") + if next_comma_position == -1: + value = value_remainder + else: + value = value_remainder[:next_comma_position] + + out_data[selected_index] = value.rstrip()[:MAX_OUTPUT_VALUE_LENGTH] + out_found[selected_index] = True + + fatal( + audit, + [ + "EOF encountered on eplusout.eso while reading data", + "probable EnergyPlus error condition -- check eplusout.err", + "ReadVarsESO program terminated.", + ], + ) + + +def processing_error(output_file_name: str, line: str, audit: TextIO | None) -> None: + fatal( + audit, + [ + f"Output file={output_file_name}", + "error occurred during processing.", + "Apparent line in error (1st 50 characters):", + line[:50].rstrip(), + "ReadVarsESO program terminated.", + ], + ) + + +def elapsed_string(start_time: float) -> str: + elapsed = time.process_time() - start_time + hours = int(elapsed // 3600) + elapsed -= hours * 3600 + minutes = int(elapsed // 60) + seconds = elapsed - minutes * 60 + return f"{hours:02d}hr {minutes:02d}min {seconds:5.2f}sec" + + +def run(argv: list[str]) -> int: + display_string("ReadVarsESO program starting.") + start_time = time.process_time() + audit: TextIO | None = None + output: TextIO | None = None + errors_happened = False + + try: + audit = Path("readvars.audit").open("a", encoding="utf-8") + audit_write(audit, "ReadVarsESO") + + options = parse_options(argv) + input_file_name, output_file_name, separator, get_vars_from_eso, rvi_lines = read_rvi_configuration(options, audit) + + try: + output = Path(output_file_name).open("w", encoding="utf-8", newline="") + except OSError: + fatal( + audit, + [ + f"Output file={output_file_name}", + "cannot be opened. It may be open in another program.", + "Please close it and try again.", + "ReadVarsESO program terminated.", + ], + ) + audit_write(audit, f"output file:{output_file_name}") + + requests = Requests([], [], [], [], []) + if not get_vars_from_eso: + requests = parse_rvi_variable_requests(rvi_lines, audit) + if ( + not requests.track_numbers + and not requests.find_variables + and not requests.ignore_numbers + and not requests.ignore_find_variables + ): + display_string("You chose no variables") + audit_write(audit, "You chose no variables") + raise EarlyExit + if not requests.track_numbers and not requests.find_variables: + get_vars_from_eso = True + + if get_vars_from_eso: + audit_write(audit, f"getting all vars from:{input_file_name}") + + eso_lines = read_lines(Path(input_file_name)) + records, dictionary_end_index = dictionary_records(eso_lines, audit) + selected = select_variables(records, requests, get_vars_from_eso, options.frequency, options.limited, audit) + audit_write(audit, f" number variables requested for output={len(selected)}") + + write_header(output, selected, separator, options.fix_header, audit) + data_start_index = dictionary_end_index + 2 + process_data_records(eso_lines, data_start_index, selected, output, output_file_name, separator, audit) + + except EarlyExit: + pass + except ReadVarsFatal as err: + errors_happened = True + return err.exit_code + finally: + if output is not None: + output.close() + + if audit is not None: + runtime_message = f"ReadVars Run Time={elapsed_string(start_time)}" + display_string(runtime_message) + audit_write(audit, runtime_message) + if not errors_happened: + display_string("ReadVarsESO program completed successfully.") + audit_write(audit, "ReadVarsESO program completed successfully.") + audit.close() + + return 0 + + +def main() -> None: + sys.exit(run(sys.argv[1:])) + + +if __name__ == "__main__": + main() diff --git a/testfiles/AdvancedOutput/ExerciseOutput1-CustomCSV.bat b/testfiles/AdvancedOutput/ExerciseOutput1-CustomCSV.bat index 7de5fbbcdb4..684cdf29c40 100644 --- a/testfiles/AdvancedOutput/ExerciseOutput1-CustomCSV.bat +++ b/testfiles/AdvancedOutput/ExerciseOutput1-CustomCSV.bat @@ -47,27 +47,27 @@ IF EXIST eplusout.csv DEL eplusout.csv @echo . @echo ===== Extracting Results @echo . -IF EXIST "%2.rvi" %post_proc%ReadVarsESO.exe "%2.rvi" timestep +IF EXIST "%2.rvi" %post_proc%ReadVarsESO.bat "%2.rvi" timestep IF EXIST eplusout.csv MOVE eplusout.csv "%1_%2_timestep.csv" -IF EXIST "%2.rvi" %post_proc%ReadVarsESO.exe "%2.rvi" hourly +IF EXIST "%2.rvi" %post_proc%ReadVarsESO.bat "%2.rvi" hourly IF EXIST eplusout.csv MOVE eplusout.csv "%1_%2_hourly.csv" -IF EXIST "%2.rvi" %post_proc%ReadVarsESO.exe "%2.rvi" daily +IF EXIST "%2.rvi" %post_proc%ReadVarsESO.bat "%2.rvi" daily IF EXIST eplusout.csv MOVE eplusout.csv "%1_%2_daily.csv" -IF EXIST "%2.rvi" %post_proc%ReadVarsESO.exe "%2.rvi" monthly +IF EXIST "%2.rvi" %post_proc%ReadVarsESO.bat "%2.rvi" monthly IF EXIST eplusout.csv MOVE eplusout.csv "%1_%2_monthly.csv" -IF EXIST "%2.rvi" %post_proc%ReadVarsESO.exe "%2.rvi" annual +IF EXIST "%2.rvi" %post_proc%ReadVarsESO.bat "%2.rvi" annual IF EXIST eplusout.csv MOVE eplusout.csv "%1_%2_annual.csv" @echo . -IF EXIST "%2.mvi" %post_proc%ReadVarsESO.exe "%2.mvi" timestep +IF EXIST "%2.mvi" %post_proc%ReadVarsESO.bat "%2.mvi" timestep IF EXIST eplusout.csv MOVE eplusout.csv "%1_%2_Meter_timestep.csv" -IF EXIST "%2.mvi" %post_proc%ReadVarsESO.exe "%2.mvi" hourly +IF EXIST "%2.mvi" %post_proc%ReadVarsESO.bat "%2.mvi" hourly IF EXIST eplusout.csv MOVE eplusout.csv "%1_%2_Meter_hourly.csv" -IF EXIST "%2.mvi" %post_proc%ReadVarsESO.exe "%2.mvi" daily +IF EXIST "%2.mvi" %post_proc%ReadVarsESO.bat "%2.mvi" daily IF EXIST eplusout.csv MOVE eplusout.csv "%1_%2_Meter_daily.csv" -IF EXIST "%2.mvi" %post_proc%ReadVarsESO.exe "%2.mvi" monthly +IF EXIST "%2.mvi" %post_proc%ReadVarsESO.bat "%2.mvi" monthly IF EXIST eplusout.csv MOVE eplusout.csv "%1_%2_Meter_monthly.csv" -IF EXIST "%2.mvi" %post_proc%ReadVarsESO.exe "%2.mvi" annual +IF EXIST "%2.mvi" %post_proc%ReadVarsESO.bat "%2.mvi" annual IF EXIST eplusout.csv MOVE eplusout.csv "%1_%2_Meter_annual.csv" @echo . From 193c8b9f5e52334b6f229b8f787c4d4d8c88a313 Mon Sep 17 00:00:00 2001 From: "Jason W. DeGraw" Date: Mon, 18 May 2026 21:05:40 -0400 Subject: [PATCH 2/7] Add variable listing features --- src/ReadVars/CMakeLists.txt | 5 + src/ReadVars/ReadVarsESO.py | 284 ++++++++++++++++++- src/ReadVars/test_readvars_cli.py | 82 ++++++ src/ReadVars/testdata/readvars_discovery.eso | 19 ++ 4 files changed, 386 insertions(+), 4 deletions(-) create mode 100644 src/ReadVars/test_readvars_cli.py create mode 100644 src/ReadVars/testdata/readvars_discovery.eso diff --git a/src/ReadVars/CMakeLists.txt b/src/ReadVars/CMakeLists.txt index 786053bfeac..4313975dc3c 100644 --- a/src/ReadVars/CMakeLists.txt +++ b/src/ReadVars/CMakeLists.txt @@ -46,3 +46,8 @@ endif() install(FILES "${READVARS_SCRIPT}" DESTINATION PostProcess) install(PROGRAMS "${READVARS_WRAPPER}" DESTINATION PostProcess) + +if(BUILD_TESTING) + add_test(NAME ReadVarsESO.CLI COMMAND "${Python_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/test_readvars_cli.py") + set_tests_properties(ReadVarsESO.CLI PROPERTIES LABELS ReadVars) +endif() diff --git a/src/ReadVars/ReadVarsESO.py b/src/ReadVars/ReadVarsESO.py index d87764cd7eb..19214a269da 100644 --- a/src/ReadVars/ReadVarsESO.py +++ b/src/ReadVars/ReadVarsESO.py @@ -9,7 +9,10 @@ from __future__ import annotations +import argparse +import csv from dataclasses import dataclass +import json from pathlib import Path import sys import time @@ -28,6 +31,24 @@ 5: "!RunPeriod", } +FREQUENCY_ALIASES = { + "timestep": 1, + "time-step": 1, + "detailed": 1, + "detail": 1, + "hourly": 2, + "daily": 3, + "monthly": 4, + "annual": 5, + "runperiod": 5, + "run-period": 5, +} + +MODERN_COMMAND_ALIASES = { + "--list": "list", + "--variables": "list", +} + MONTHS = [ "January", "February", @@ -67,6 +88,11 @@ class DictionaryRecord: number: int line: str label: str + key: str = "" + variable: str = "" + units: str = "" + frequency: str = "" + raw_frequency: str = "" @dataclass @@ -134,6 +160,62 @@ def parse_options(argv: list[str]) -> Options: return Options(var_file_name, get_vars_from_eso, frequency, limited, fix_header) +def is_modern_cli(argv: list[str]) -> bool: + if not argv: + return False + + first = argv[0].lower() + return first in {"list", "variables", "-h", "--help"} or first in MODERN_COMMAND_ALIASES + + +def normalized_modern_args(argv: list[str]) -> list[str]: + if not argv: + return argv + + first = argv[0].lower() + replacement = MODERN_COMMAND_ALIASES.get(first) + if replacement is None: + return argv + + return [replacement] + argv[1:] + + +def build_modern_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="ReadVarsESO", + description="Convert EnergyPlus ESO/MTR files or inspect their available output variables.", + epilog=( + "Examples:\n" + " ReadVarsESO list eplusout.eso --frequency hourly --search temperature\n" + " ReadVarsESO --list eplusout.eso --format csv" + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + subparsers = parser.add_subparsers(dest="command") + + list_parser = subparsers.add_parser("list", aliases=["variables"], help="List variables in an ESO/MTR data dictionary.") + list_parser.add_argument("input_file", nargs="?", default="eplusout.eso", help="ESO/MTR file to inspect.") + list_parser.add_argument( + "-f", + "--frequency", + choices=sorted(FREQUENCY_ALIASES), + help="Only show variables reported at this frequency.", + ) + list_parser.add_argument( + "-s", + "--search", + help="Only show variables whose key, variable name, units, frequency, or label contains this text.", + ) + list_parser.add_argument( + "--format", + choices=["table", "csv", "json"], + default="table", + help="Output format.", + ) + + return parser + + def read_lines(path: Path) -> list[str]: with path.open("r", encoding="utf-8-sig", errors="replace") as handle: return [line.rstrip("\r\n") for line in handle] @@ -373,6 +455,70 @@ def build_header_label(line: str) -> str: return label.replace(",", ":") +def split_variable_units(variable_with_units: str) -> tuple[str, str]: + variable_with_units = variable_with_units.strip() + close_position = variable_with_units.rfind("]") + open_position = variable_with_units.rfind("[", 0, close_position) + if open_position == -1 or close_position == -1: + return variable_with_units, "" + + variable = variable_with_units[:open_position].rstrip() + units = variable_with_units[open_position + 1 : close_position].strip() + return variable, units + + +def normalized_frequency(raw_frequency: str) -> str: + raw_frequency = raw_frequency.strip() + bracket_position = raw_frequency.find("[") + if bracket_position != -1: + raw_frequency = raw_frequency[:bracket_position].rstrip() + + comma_position = raw_frequency.find(",") + if comma_position != -1: + raw_frequency = raw_frequency[:comma_position].rstrip() + + return raw_frequency + + +def parse_dictionary_record(line: str) -> DictionaryRecord | None: + number = parse_report_number(line) + if number is None or number <= 5: + return None + + fields = line.split(",", 2) + if len(fields) < 3: + return DictionaryRecord(number, line, build_header_label(line)) + + remainder = fields[2] + bang_position = remainder.find("!") + if bang_position == -1: + definition = remainder.strip() + raw_frequency = "" + else: + definition = remainder[:bang_position].strip() + raw_frequency = remainder[bang_position + 1 :].strip() + + comma_position = definition.find(",") + if comma_position == -1: + key = "" + variable_with_units = definition + else: + key = definition[:comma_position].strip() + variable_with_units = definition[comma_position + 1 :].strip() + + variable, units = split_variable_units(variable_with_units) + return DictionaryRecord( + number=number, + line=line, + label=build_header_label(line), + key=key, + variable=variable, + units=units, + frequency=normalized_frequency(raw_frequency), + raw_frequency=raw_frequency, + ) + + def dictionary_records(eso_lines: list[str], audit: TextIO | None) -> tuple[list[DictionaryRecord], int]: end_index = None for index, line in enumerate(eso_lines): @@ -392,10 +538,9 @@ def dictionary_records(eso_lines: list[str], audit: TextIO | None) -> tuple[list records: list[DictionaryRecord] = [] for line in eso_lines[:end_index]: - number = parse_report_number(line) - if number is None or number <= 5: - continue - records.append(DictionaryRecord(number, line, build_header_label(line))) + record = parse_dictionary_record(line) + if record is not None: + records.append(record) return records, end_index @@ -567,6 +712,134 @@ def select_variables( return selected_with_limit(selected, limited, audit) +def record_matches_search(record: DictionaryRecord, search_text: str | None) -> bool: + if not search_text: + return True + + haystack = " ".join( + [ + record.key, + record.variable, + record.units, + record.frequency, + record.label, + record.line, + ] + ).upper() + return search_text.upper() in haystack + + +def is_dictionary_time_stamp_record(record: DictionaryRecord) -> bool: + raw_frequency = record.raw_frequency.strip().upper() + return raw_frequency.startswith("WHEN ") and raw_frequency.endswith("REQUESTED") + + +def records_for_modern_cli(input_file: str) -> list[DictionaryRecord]: + input_path = Path(input_file) + if not input_path.is_file(): + raise ReadVarsFatal([f"Input file does not exist: {input_file}"]) + + records, _ = dictionary_records(read_lines(input_path), None) + return [record for record in records if not is_dictionary_time_stamp_record(record)] + + +def truncate_for_table(value: object, width: int) -> str: + text = str(value) + if len(text) <= width: + return text + if width <= 3: + return text[:width] + return text[: width - 3] + "..." + + +def print_table(rows: list[dict[str, object]], columns: list[tuple[str, str, int]], empty_message: str) -> None: + if not rows: + print(empty_message) + return + + widths = [] + for header, key, maximum_width in columns: + content_width = max(len(str(row.get(key, ""))) for row in rows) + widths.append(min(max(len(header), content_width), maximum_width)) + + header_line = " ".join(header.ljust(widths[index]) for index, (header, _, _) in enumerate(columns)) + divider_line = " ".join(("-" * widths[index]) for index in range(len(columns))) + print(header_line) + print(divider_line) + + for row in rows: + values = [] + for index, (_, key, _) in enumerate(columns): + values.append(truncate_for_table(row.get(key, ""), widths[index]).ljust(widths[index])) + print(" ".join(values)) + + +def dictionary_record_as_row(record: DictionaryRecord) -> dict[str, object]: + return { + "number": record.number, + "frequency": record.frequency, + "key": record.key, + "variable": record.variable, + "units": record.units, + "label": record.label, + } + + +def write_records(records: list[DictionaryRecord], output_format: str) -> None: + rows = [dictionary_record_as_row(record) for record in records] + + if output_format == "json": + print(json.dumps(rows, indent=2)) + return + + if output_format == "csv": + writer = csv.DictWriter( + sys.stdout, + fieldnames=["number", "frequency", "key", "variable", "units", "label"], + lineterminator="\n", + ) + writer.writeheader() + writer.writerows(rows) + return + + print_table( + rows, + [ + ("Report #", "number", 8), + ("Frequency", "frequency", 12), + ("Key", "key", 36), + ("Variable", "variable", 70), + ("Units", "units", 24), + ], + "No variables matched.", + ) + + +def run_modern_cli(argv: list[str]) -> int: + parser = build_modern_parser() + args = parser.parse_args(normalized_modern_args(argv)) + if args.command is None: + parser.print_help() + return 0 + + try: + records = records_for_modern_cli(args.input_file) + except ReadVarsFatal as err: + for message in err.messages: + print(message, file=sys.stderr) + return err.exit_code + + if args.command in {"list", "variables"}: + if args.frequency: + records = [record for record in records if is_allowed_frequency(record.line, FREQUENCY_ALIASES[args.frequency])] + records = [record for record in records if record_matches_search(record, args.search)] + write_records(records, args.format) + return 0 + + parser.print_help() + return 1 + + def write_header(output: TextIO, selected: list[SelectedVariable], separator: str, fix_header: bool, audit: TextIO | None) -> None: header_parts = ["Date/Time"] for variable in selected: @@ -800,6 +1073,9 @@ def elapsed_string(start_time: float) -> str: def run(argv: list[str]) -> int: + if is_modern_cli(argv): + return run_modern_cli(argv) + display_string("ReadVarsESO program starting.") start_time = time.process_time() audit: TextIO | None = None diff --git a/src/ReadVars/test_readvars_cli.py b/src/ReadVars/test_readvars_cli.py new file mode 100644 index 00000000000..bf2c9c2f416 --- /dev/null +++ b/src/ReadVars/test_readvars_cli.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +from pathlib import Path +import json +import shutil +import subprocess +import sys + + +SCRIPT = Path(__file__).with_name("ReadVarsESO.py") +TEST_ESO = Path(__file__).parent / "testdata" / "readvars_discovery.eso" + + +def run_readvars(args, cwd=None): + return subprocess.run( + [sys.executable, str(SCRIPT)] + args, + cwd=cwd, + check=True, + capture_output=True, + text=True, + ) + + +def test_list_command(): + result = run_readvars( + [ + "list", + str(TEST_ESO), + "--frequency", + "hourly", + "--search", + "temperature", + "--format", + "json", + ] + ) + rows = json.loads(result.stdout) + assert len(rows) == 1 + assert rows[0]["number"] == 8 + assert rows[0]["frequency"] == "Hourly" + assert rows[0]["key"] == "ZONE ONE" + assert rows[0]["variable"] == "Zone Mean Air Temperature" + assert rows[0]["units"] == "C" + + +def test_list_command_filters_time_stamp_records(): + result = run_readvars(["list", str(TEST_ESO), "--format", "json"]) + rows = json.loads(result.stdout) + assert [row["number"] for row in rows] == [7, 8, 9] + assert all("When Annual Report Variables Requested" not in row["frequency"] for row in rows) + assert all(row["variable"] != "Calendar Year of Simulation" for row in rows) + + +def test_legacy_conversion_still_works(): + run_dir = Path.cwd() / "readvars_cli_test_runtime" + if run_dir.exists(): + shutil.rmtree(run_dir) + run_dir.mkdir() + try: + shutil.copy(TEST_ESO, run_dir / "eplusout.eso") + (run_dir / "custom.rvi").write_text( + "eplusout.eso\n" + "custom.csv\n" + "ZONE ONE,Zone Mean Air Temperature\n", + encoding="utf-8", + ) + + run_readvars(["custom.rvi", "hourly", "fixheader"], cwd=run_dir) + output = (run_dir / "custom.csv").read_text(encoding="utf-8") + assert output.splitlines() == [ + "Date/Time,ZONE ONE:Zone Mean Air Temperature [C](Hourly)", + " 01/01 01:00:00,20.0 ", + " 01/01 02:00:00,21.0 ", + ] + finally: + shutil.rmtree(run_dir, ignore_errors=True) + + +if __name__ == "__main__": + test_list_command() + test_list_command_filters_time_stamp_records() + test_legacy_conversion_still_works() diff --git a/src/ReadVars/testdata/readvars_discovery.eso b/src/ReadVars/testdata/readvars_discovery.eso new file mode 100644 index 00000000000..a65dc330d87 --- /dev/null +++ b/src/ReadVars/testdata/readvars_discovery.eso @@ -0,0 +1,19 @@ +Program Version,EnergyPlus, ReadVars Discovery Test +1,5,Environment Title[],Latitude[degrees],Longitude[degrees],Time Zone[],Elevation[m] +2,8,Day of Simulation[],Month[],Day of Month[],DST Indicator[1=yes 0=no],Hour[],StartMinute[],EndMinute[],DayType +3,5,Cumulative Day of Simulation[],Month[],Day of Month[],DST Indicator[1=yes 0=no],DayType +4,2,Cumulative Days of Simulation[],Month[] +5,1,Cumulative Days of Simulation[] +6,1,Calendar Year of Simulation[] ! When Annual Report Variables Requested +7,2,Environment,Outdoor Dry Bulb [C] !Hourly +8,2,ZONE ONE,Zone Mean Air Temperature [C] !Hourly +9,2,ZONE ONE,Zone Air System Sensible Heating Rate [W] !TimeStep +End of Data Dictionary +1,DISCOVERY TEST ENVIRONMENT,0,0,0,0 +2,1,1,1,0,1,0.00,60.00,Monday +7,-5.0 +8,20.0 +2,1,1,1,0,2,0.00,60.00,Monday +7,-4.0 +8,21.0 +End of Data From a57969ddd46fe77aa2f9566a80ceef7c3b889d9c Mon Sep 17 00:00:00 2001 From: "Jason W. DeGraw" Date: Wed, 20 May 2026 01:31:49 -0400 Subject: [PATCH 3/7] More command line features --- src/ReadVars/ReadVarsESO.py | 143 +++++++++++++++++++++++++----- src/ReadVars/test_readvars_cli.py | 53 +++++++++++ 2 files changed, 176 insertions(+), 20 deletions(-) diff --git a/src/ReadVars/ReadVarsESO.py b/src/ReadVars/ReadVarsESO.py index 19214a269da..b9ba3e753bf 100644 --- a/src/ReadVars/ReadVarsESO.py +++ b/src/ReadVars/ReadVarsESO.py @@ -2,9 +2,8 @@ """Read EnergyPlus ESO/MTR output files and write delimited tables. This is a Python port of the historical ReadVarsESO Fortran utility. It keeps -the same command-line interface: an optional RVI/MVI file, optional frequency -filter, optional ``unlimited``/``nolimit`` column handling, and optional -``fixheader`` output. +the same legacy command-line interface while also exposing modern ``list`` and +``read`` subcommands for inspecting and converting ESO/MTR files directly. """ from __future__ import annotations @@ -44,9 +43,23 @@ "run-period": 5, } +MODERN_FREQUENCY_ALIASES = { + "timestep": {"timestep", "time step", "time-step"}, + "time-step": {"timestep", "time step", "time-step"}, + "detailed": {"detailed"}, + "detail": {"detailed"}, + "hourly": {"hourly"}, + "daily": {"daily"}, + "monthly": {"monthly"}, + "annual": {"annual"}, + "runperiod": {"runperiod", "run period", "environment"}, + "run-period": {"runperiod", "run period", "environment"}, +} + MODERN_COMMAND_ALIASES = { "--list": "list", "--variables": "list", + "--read": "read", } MONTHS = [ @@ -165,7 +178,7 @@ def is_modern_cli(argv: list[str]) -> bool: return False first = argv[0].lower() - return first in {"list", "variables", "-h", "--help"} or first in MODERN_COMMAND_ALIASES + return first in {"list", "variables", "read", "-h", "--help"} or first in MODERN_COMMAND_ALIASES def normalized_modern_args(argv: list[str]) -> list[str]: @@ -187,7 +200,8 @@ def build_modern_parser() -> argparse.ArgumentParser: epilog=( "Examples:\n" " ReadVarsESO list eplusout.eso --frequency hourly --search temperature\n" - " ReadVarsESO --list eplusout.eso --format csv" + " ReadVarsESO --list eplusout.eso --format csv\n" + " ReadVarsESO read eplusout.eso --output hourly.csv --frequency hourly" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) @@ -198,7 +212,7 @@ def build_modern_parser() -> argparse.ArgumentParser: list_parser.add_argument( "-f", "--frequency", - choices=sorted(FREQUENCY_ALIASES), + choices=sorted(MODERN_FREQUENCY_ALIASES), help="Only show variables reported at this frequency.", ) list_parser.add_argument( @@ -213,6 +227,25 @@ def build_modern_parser() -> argparse.ArgumentParser: help="Output format.", ) + read_parser = subparsers.add_parser("read", help="Convert an ESO/MTR file to CSV.") + read_parser.add_argument("input_file", nargs="?", default="eplusout.eso", help="ESO/MTR file to convert.") + read_parser.add_argument( + "-o", + "--output", + help="CSV output file. Defaults to the input file name with a .csv extension.", + ) + read_parser.add_argument( + "-f", + "--frequency", + choices=sorted(MODERN_FREQUENCY_ALIASES), + help="Only include variables reported at this frequency.", + ) + read_parser.add_argument( + "-s", + "--search", + help="Only include variables whose key, variable name, units, frequency, or label contains this text.", + ) + return parser @@ -550,6 +583,14 @@ def is_allowed_frequency(line: str, frequency: int) -> bool: return marker is None or marker in line +def record_matches_modern_frequency(record: DictionaryRecord, frequency_alias: str | None) -> bool: + if not frequency_alias: + return True + + normalized = record.frequency.strip().lower().replace("-", " ") + return normalized in MODERN_FREQUENCY_ALIASES[frequency_alias] + + def myindex(text: str, substring: str) -> int: if not substring: return -1 @@ -743,6 +784,65 @@ def records_for_modern_cli(input_file: str) -> list[DictionaryRecord]: return [record for record in records if not is_dictionary_time_stamp_record(record)] +def filter_modern_records( + records: list[DictionaryRecord], + frequency: str | None, + search: str | None, +) -> list[DictionaryRecord]: + filtered = [record for record in records if not is_dictionary_time_stamp_record(record)] + + if frequency: + filtered = [record for record in filtered if record_matches_modern_frequency(record, frequency)] + + return [record for record in filtered if record_matches_search(record, search)] + + +def default_modern_output_file(input_file: str) -> Path: + return Path(input_file).with_suffix(".csv") + + +def convert_modern_read( + input_file: str, + output_file: str | None, + frequency: str | None, + search: str | None, +) -> Path: + input_path = Path(input_file) + if not input_path.is_file(): + raise ReadVarsFatal([f"Input file does not exist: {input_file}"]) + + output_path = Path(output_file) if output_file else default_modern_output_file(input_file) + eso_lines = read_lines(input_path) + records, dictionary_end_index = dictionary_records(eso_lines, None) + selected = [ + SelectedVariable(record.number, record.label, True) + for record in filter_modern_records(records, frequency, search) + ] + + try: + with output_path.open("w", encoding="utf-8", newline="") as output: + write_header(output, selected, ",", True, None) + process_data_records( + eso_lines, + dictionary_end_index + 2, + selected, + output, + str(output_path), + ",", + None, + legacy_spacing=False, + ) + except OSError as err: + raise ReadVarsFatal( + [ + f"Output file cannot be opened: {output_path}", + str(err), + ] + ) from err + + return output_path + + def truncate_for_table(value: object, width: int) -> str: text = str(value) if len(text) <= width: @@ -823,19 +923,20 @@ def run_modern_cli(argv: list[str]) -> int: return 0 try: - records = records_for_modern_cli(args.input_file) + if args.command in {"list", "variables"}: + records = filter_modern_records(records_for_modern_cli(args.input_file), args.frequency, args.search) + write_records(records, args.format) + return 0 + + if args.command == "read": + output_path = convert_modern_read(args.input_file, args.output, args.frequency, args.search) + print(f"Wrote {output_path}") + return 0 except ReadVarsFatal as err: for message in err.messages: print(message, file=sys.stderr) return err.exit_code - if args.command in {"list", "variables"}: - if args.frequency: - records = [record for record in records if is_allowed_frequency(record.line, FREQUENCY_ALIASES[args.frequency])] - records = [record for record in records if record_matches_search(record, args.search)] - write_records(records, args.format) - return 0 - parser.print_help() return 1 @@ -883,6 +984,7 @@ def flush_row( out_data: list[str], out_found: list[bool], separator: str, + legacy_spacing: bool = True, ) -> None: if not any(out_found): return @@ -891,7 +993,7 @@ def flush_row( for index, found in enumerate(out_found): row_parts.append(out_data[index].rstrip() if found else "") output.write(separator.join(row_parts)) - output.write(" \n") + output.write(" \n" if legacy_spacing else "\n") for index in range(len(out_data)): out_data[index] = "" @@ -906,6 +1008,7 @@ def process_data_records( output_file_name: str, separator: str, audit: TextIO | None, + legacy_spacing: bool = True, ) -> None: out_data = [""] * len(selected) out_found = [False] * len(selected) @@ -931,7 +1034,7 @@ def process_data_records( label = current_month else: label = current_period - flush_row(output, label, out_data, out_found, separator) + flush_row(output, label, out_data, out_found, separator, legacy_spacing) return if line.strip() == "": @@ -972,7 +1075,7 @@ def process_data_records( end_minute = fields[7] if current_date and (hour_of_day != previous_hour_of_day or end_minute != previous_end_minute): - flush_row(output, current_date, out_data, out_found, separator) + flush_row(output, current_date, out_data, out_found, separator, legacy_spacing) previous_hour_of_day = hour_of_day previous_end_minute = end_minute @@ -988,7 +1091,7 @@ def process_data_records( processing_error(output_file_name, line, audit) if current_month_day: - flush_row(output, current_month_day, out_data, out_found, separator) + flush_row(output, current_month_day, out_data, out_found, separator, legacy_spacing) month = int(fields[2]) day = int(fields[3]) @@ -1004,7 +1107,7 @@ def process_data_records( processing_error(output_file_name, line, audit) if current_month: - flush_row(output, current_month, out_data, out_found, separator) + flush_row(output, current_month, out_data, out_found, separator, legacy_spacing) month_index = int(fields[2]) - 1 if month_index < 0 or month_index >= len(MONTHS): @@ -1020,7 +1123,7 @@ def process_data_records( processing_error(output_file_name, line, audit) if current_period: - flush_row(output, current_period, out_data, out_found, separator) + flush_row(output, current_period, out_data, out_found, separator, legacy_spacing) current_period = f"simdays={int(fields[1])}" continue diff --git a/src/ReadVars/test_readvars_cli.py b/src/ReadVars/test_readvars_cli.py index bf2c9c2f416..2ecac8d52b9 100644 --- a/src/ReadVars/test_readvars_cli.py +++ b/src/ReadVars/test_readvars_cli.py @@ -51,6 +51,57 @@ def test_list_command_filters_time_stamp_records(): assert all(row["variable"] != "Calendar Year of Simulation" for row in rows) +def test_read_command_converts_everything_by_default(): + run_dir = Path.cwd() / "readvars_read_all_runtime" + if run_dir.exists(): + shutil.rmtree(run_dir) + run_dir.mkdir() + try: + shutil.copy(TEST_ESO, run_dir / "eplusout.eso") + + run_readvars(["read", "eplusout.eso"], cwd=run_dir) + output = (run_dir / "eplusout.csv").read_text(encoding="utf-8") + assert output.splitlines() == [ + "Date/Time,Environment:Outdoor Dry Bulb [C](Hourly),ZONE ONE:Zone Mean Air Temperature [C](Hourly),ZONE ONE:Zone Air System Sensible Heating Rate [W](TimeStep)", + " 01/01 01:00:00,-5.0,20.0,", + " 01/01 02:00:00,-4.0,21.0,", + ] + assert not (run_dir / "readvars.audit").exists() + finally: + shutil.rmtree(run_dir, ignore_errors=True) + + +def test_read_command_supports_output_and_filters(): + run_dir = Path.cwd() / "readvars_read_filtered_runtime" + if run_dir.exists(): + shutil.rmtree(run_dir) + run_dir.mkdir() + try: + shutil.copy(TEST_ESO, run_dir / "eplusout.eso") + + run_readvars( + [ + "read", + "eplusout.eso", + "--output", + "selected.csv", + "--frequency", + "hourly", + "--search", + "temperature", + ], + cwd=run_dir, + ) + output = (run_dir / "selected.csv").read_text(encoding="utf-8") + assert output.splitlines() == [ + "Date/Time,ZONE ONE:Zone Mean Air Temperature [C](Hourly)", + " 01/01 01:00:00,20.0", + " 01/01 02:00:00,21.0", + ] + finally: + shutil.rmtree(run_dir, ignore_errors=True) + + def test_legacy_conversion_still_works(): run_dir = Path.cwd() / "readvars_cli_test_runtime" if run_dir.exists(): @@ -79,4 +130,6 @@ def test_legacy_conversion_still_works(): if __name__ == "__main__": test_list_command() test_list_command_filters_time_stamp_records() + test_read_command_converts_everything_by_default() + test_read_command_supports_output_and_filters() test_legacy_conversion_still_works() From 40b6fb34f4aa5d33f123776d492f675bcec29e0b Mon Sep 17 00:00:00 2001 From: "Jason W. DeGraw" Date: Thu, 18 Jun 2026 06:50:57 -0400 Subject: [PATCH 4/7] Initial draft of NFP --- .../FY2026/NFP-ReadVars-Python-Replacement.md | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 design/FY2026/NFP-ReadVars-Python-Replacement.md diff --git a/design/FY2026/NFP-ReadVars-Python-Replacement.md b/design/FY2026/NFP-ReadVars-Python-Replacement.md new file mode 100644 index 00000000000..d950a32b918 --- /dev/null +++ b/design/FY2026/NFP-ReadVars-Python-Replacement.md @@ -0,0 +1,95 @@ +Replace Fortran ReadVars with Python ReadVarsESO +================ + +**EnergyPlus Development Team** + + - Original Date: 06/18/2026 + - Revision Date: 06/18/2026 + + +## Justification for New Feature ## + +ReadVarsESO is a post-processing utility used to convert EnergyPlus ESO and MTR output files into delimited tabular output. The current implementation is written in modern Fortran, which keeps a Fortran build dependency alive for a small utility that is otherwise independent of the simulation engine. + +This proposal replaces the Fortran ReadVarsESO program with a vanilla Python implementation. The goal is to preserve the existing command-line behavior for current workflows while making the tool easier to maintain, test, package, and extend. + +## E-mail and Conference Call Conclusions ## + +N/A + +## Overview ## + +The new ReadVarsESO implementation will be a Python script distributed with EnergyPlus and wrapped by platform-appropriate launcher scripts. Existing RVI/MVI based conversion workflows should continue to work, including frequency filters, unlimited column handling, and fixed header behavior. + +The Python version will also provide a modern subcommand-based interface for direct inspection and conversion of ESO/MTR files. Initial modern commands include listing available output variables and reading data directly to CSV without requiring a separate RVI/MVI file. + +### Legacy behavior to preserve ### + +The historical ReadVarsESO interface is positional and file-oriented. When called with no arguments, it reads `eplusout.eso`, selects all available variables from the ESO data dictionary, and writes `eplusout.csv`. When called with an RVI/MVI file, the control file identifies the input ESO/MTR file, the output file, and the requested variables. + +Legacy variable requests may be made by report number or by matching variable names from the data dictionary. The RVI/MVI file can also exclude variables by prefixing report numbers or variable requests with `~`. Command-line options select broad output frequencies such as detailed/timestep, hourly, daily, monthly, and annual/run-period output. Existing options also include limited versus unlimited column handling and `fixheader` behavior. + +The legacy converter writes delimited output using the output file extension to determine the delimiter: comma-separated output for CSV, tab-separated output for TAB files, and space-separated output for TXT files. It also writes a `readvars.audit` file and preserves the long-standing header, timestamp, and row formatting used by existing workflows. + +### Enhancements in the Python version ### + +The Python replacement adds a modern subcommand interface while retaining the legacy interface. The `list` command, also available as `variables`, inspects an ESO/MTR data dictionary and reports the available report number, frequency, key, variable name, units, and legacy label. It supports table, CSV, and JSON output, plus filtering by reporting frequency and search text. Internal timestamp dictionary records, such as annual calendar-year records used only to label report periods, are omitted from the modern list output. + +The new `read` command converts an ESO/MTR file directly to CSV without requiring an RVI/MVI file. Calling `ReadVarsESO read ` selects all user-reportable variables and writes a CSV file next to the input using the same base file name. The output file can be configured with `--output`, and conversion can be narrowed with the same frequency and search filters used by `list`. + +The modern commands are intended to be easier to script and discover. They avoid legacy audit-file side effects where practical, provide structured output for tooling, and make it possible for users to inspect available variables before selecting data to convert. + +## Approach ## + +The implementation will port the existing ReadVarsESO behavior to a Python script with no third-party package dependencies. CMake will copy the Python script and wrapper into the EnergyPlus runtime and install locations in place of building a Fortran executable. + +The legacy interface will remain available for compatibility. A modern interface will be added alongside it, including: + +- `list` or `variables` to inspect available report variables, keys, units, and reporting frequencies. +- `read` to convert an ESO/MTR file directly to CSV. +- Command options for output file selection and basic filtering by frequency or search text. + +The modern commands will avoid legacy audit-file side effects where practical and will omit internal timestamp dictionary records that are not user-reportable output variables. Shared parsing and conversion routines will be used where possible so that legacy and modern behavior stay aligned. + +## Testing/Validation/Data Sources ## + +Testing will include focused unit or script-level tests for: + +- Legacy RVI/MVI conversion compatibility. +- Listing variables and filtering metadata records. +- Listing variables in table, CSV, and JSON formats. +- Direct modern CSV conversion using the `read` command. +- Configurable output paths and basic frequency/search filtering. +- Default `read` behavior that converts all user-reportable variables when only an input file is supplied. + +Existing EnergyPlus regression workflows that invoke ReadVarsESO should continue to pass using the Python wrapper. + +## Input Output Reference Documentation ## + +The Input Output Reference is not expected to require changes because this feature does not add, remove, or modify EnergyPlus input objects. + +The auxiliary programs documentation should be updated to describe both the legacy ReadVarsESO usage and the modern `list` and `read` subcommands. + +## Input Description ## + +No IDD, epJSON schema, or input object changes are required. + +## Outputs Description ## + +No simulation output variables are added or changed. + +ReadVarsESO output files should remain compatible with existing CSV, TAB, and TXT post-processing workflows for the legacy interface. The modern `read` command will produce CSV output directly, with the output file name configurable by command-line option. + +## Engineering Reference ## + +No engineering reference changes are expected. This is a post-processing utility replacement and does not alter simulation algorithms. + +## Example File and Transition Changes ## + +No example file changes are expected. + +No IDF transition changes are required. Packaging and scripts that directly invoke the old executable name may need updates to call the Python wrapper, while existing command-line arguments should remain compatible through that wrapper. + +## References ## + +N/A From 03d3e5b71444dae0d433892bec1d9cb15397c4bb Mon Sep 17 00:00:00 2001 From: Jason DeGraw Date: Thu, 18 Jun 2026 11:21:51 -0400 Subject: [PATCH 5/7] Revise documentation for Python ReadVarsESO replacement --- .../FY2026/NFP-ReadVars-Python-Replacement.md | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/design/FY2026/NFP-ReadVars-Python-Replacement.md b/design/FY2026/NFP-ReadVars-Python-Replacement.md index d950a32b918..785242eb099 100644 --- a/design/FY2026/NFP-ReadVars-Python-Replacement.md +++ b/design/FY2026/NFP-ReadVars-Python-Replacement.md @@ -1,17 +1,16 @@ -Replace Fortran ReadVars with Python ReadVarsESO +Replace Fortran ReadVarsESO with a Python Equivalent ================ -**EnergyPlus Development Team** +Jason W. DeGraw, ORNL - Original Date: 06/18/2026 - - Revision Date: 06/18/2026 - + - Revision Date: N/A ## Justification for New Feature ## -ReadVarsESO is a post-processing utility used to convert EnergyPlus ESO and MTR output files into delimited tabular output. The current implementation is written in modern Fortran, which keeps a Fortran build dependency alive for a small utility that is otherwise independent of the simulation engine. +ReadVarsESO is a post-processing utility used to convert EnergyPlus ESO and MTR output files into delimited tabular output (primarily into comma-separated value or CSV form). The current implementation is written in modern Fortran, requiring a Fortran build dependency for a utility that is somewhat independent of the simulation engine. The utility is invoked by the engine if the `-r` flag is passed on the command line, so there is a limited relationship between the engine and the utility. Python has been a requirement for EnergyPlus development for some time now and is a somewhat easier dependency to manage. -This proposal replaces the Fortran ReadVarsESO program with a vanilla Python implementation. The goal is to preserve the existing command-line behavior for current workflows while making the tool easier to maintain, test, package, and extend. +This proposal replaces the Fortran ReadVarsESO program with a vanilla Python implementation, where "vanilla" means that there are no dependencies outside of what comes with standard Python. The goal is to preserve the existing command-line behavior for current workflows while making the tool easier to maintain, test, package, and extend. The script has outperformed the Fortran executable in all testing thus far. ## E-mail and Conference Call Conclusions ## @@ -19,13 +18,13 @@ N/A ## Overview ## -The new ReadVarsESO implementation will be a Python script distributed with EnergyPlus and wrapped by platform-appropriate launcher scripts. Existing RVI/MVI based conversion workflows should continue to work, including frequency filters, unlimited column handling, and fixed header behavior. +The new ReadVarsESO implementation is a Python script distributed with EnergyPlus and wrapped by platform-appropriate launcher scripts. Existing RVI/MVI based conversion workflows should continue to work, including frequency filters, unlimited column handling, and fixed header behavior. -The Python version will also provide a modern subcommand-based interface for direct inspection and conversion of ESO/MTR files. Initial modern commands include listing available output variables and reading data directly to CSV without requiring a separate RVI/MVI file. +The Python version provides a modern subcommand-based interface for direct inspection and conversion of ESO/MTR files. Initial modern commands include listing available output variables and reading data directly to CSV without requiring a separate RVI/MVI file. -### Legacy behavior to preserve ### +### Legacy Behavior ### -The historical ReadVarsESO interface is positional and file-oriented. When called with no arguments, it reads `eplusout.eso`, selects all available variables from the ESO data dictionary, and writes `eplusout.csv`. When called with an RVI/MVI file, the control file identifies the input ESO/MTR file, the output file, and the requested variables. +The historical ReadVarsESO interface is primarily file-oriented. When called with no arguments, it reads `eplusout.eso`, selects all available variables from the ESO data dictionary, and writes `eplusout.csv`. When called with an RVI/MVI file, the control file identifies the input ESO/MTR file, the output file, and the requested variables. Legacy variable requests may be made by report number or by matching variable names from the data dictionary. The RVI/MVI file can also exclude variables by prefixing report numbers or variable requests with `~`. Command-line options select broad output frequencies such as detailed/timestep, hourly, daily, monthly, and annual/run-period output. Existing options also include limited versus unlimited column handling and `fixheader` behavior. @@ -33,23 +32,44 @@ The legacy converter writes delimited output using the output file extension to ### Enhancements in the Python version ### -The Python replacement adds a modern subcommand interface while retaining the legacy interface. The `list` command, also available as `variables`, inspects an ESO/MTR data dictionary and reports the available report number, frequency, key, variable name, units, and legacy label. It supports table, CSV, and JSON output, plus filtering by reporting frequency and search text. Internal timestamp dictionary records, such as annual calendar-year records used only to label report periods, are omitted from the modern list output. +The Python replacement adds a modern subcommand interface while retaining the legacy interface. The new `list` command inspects an ESO/MTR data dictionary and reports the available report number, frequency, key, variable name, units, and legacy label. It supports table, CSV, and JSON output, plus filtering by reporting frequency and search text. Internal timestamp dictionary records, such as annual calendar-year records used only to label report periods, are omitted from the modern list output. The new `read` command converts an ESO/MTR file directly to CSV without requiring an RVI/MVI file. Calling `ReadVarsESO read ` selects all user-reportable variables and writes a CSV file next to the input using the same base file name. The output file can be configured with `--output`, and conversion can be narrowed with the same frequency and search filters used by `list`. The modern commands are intended to be easier to script and discover. They avoid legacy audit-file side effects where practical, provide structured output for tooling, and make it possible for users to inspect available variables before selecting data to convert. +### Performance of the Python version ### + +To investigate the performance of the Python version, a test file with an existing RVI file was selected: 5ZoneDetailedIceStorage.idf. This model includes 35 output variables and the RVI has 34 entries, and an annual simulation of the model results in an ESO file that is approximately 7.3 MB in size. The Python and Fortran versions were each run ten times on an small Windows desktop system running on a Intel Core i7-8700 @ 3.20GHz processor, resulting in the following timings: + +| | Fortran (ms) | Python (ms) | +|:-------:|:------------:|:-----------:| +| 1 | 4720.9429 | 924.1084 | +| 2 | 4649.1618 | 832.2611 | +| 3 | 4635.4731 | 831.3671 | +| 4 | 4647.0617 | 885.8676 | +| 5 | 5082.1201 | 840.9368 | +| 6 | 4631.1113 | 843.4879 | +| 7 | 4633.1652 | 835.8188 | +| 8 | 4719.2226 | 844.0984 | +| 9 | 4794.5117 | 843.9699 | +| 10 | 4707.4851 | 839.8434 | +| Average | 4722.02555 | 852.1759 | +| Std Dev | 137.3253939 | 29.61428 | + +The Python script is roughly five times faster than the Fortran executable for this test case. + ## Approach ## -The implementation will port the existing ReadVarsESO behavior to a Python script with no third-party package dependencies. CMake will copy the Python script and wrapper into the EnergyPlus runtime and install locations in place of building a Fortran executable. +The implementation ports the existing ReadVarsESO behavior to a Python script with no third-party package dependencies. CMake copies the Python script and wrapper into the EnergyPlus runtime and install locations in place of building a Fortran executable. The legacy interface will remain available for compatibility. A modern interface will be added alongside it, including: -- `list` or `variables` to inspect available report variables, keys, units, and reporting frequencies. +- `list` to inspect available report variables, keys, units, and reporting frequencies. - `read` to convert an ESO/MTR file directly to CSV. - Command options for output file selection and basic filtering by frequency or search text. -The modern commands will avoid legacy audit-file side effects where practical and will omit internal timestamp dictionary records that are not user-reportable output variables. Shared parsing and conversion routines will be used where possible so that legacy and modern behavior stay aligned. +The modern commands avoid legacy audit-file side effects where practical and omits internal timestamp dictionary records that are not user-reportable output variables. Shared parsing and conversion routines will be used where possible so that legacy and modern behavior stay aligned. ## Testing/Validation/Data Sources ## From f0103896965d937c0a4cd079c6b32af2927cab44 Mon Sep 17 00:00:00 2001 From: "Jason W. DeGraw" Date: Fri, 19 Jun 2026 09:58:12 -0400 Subject: [PATCH 6/7] Clean up interface a little --- src/ReadVars/ReadVarsESO.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ReadVars/ReadVarsESO.py b/src/ReadVars/ReadVarsESO.py index b9ba3e753bf..9ec039cf538 100644 --- a/src/ReadVars/ReadVarsESO.py +++ b/src/ReadVars/ReadVarsESO.py @@ -58,7 +58,6 @@ MODERN_COMMAND_ALIASES = { "--list": "list", - "--variables": "list", "--read": "read", } @@ -178,7 +177,7 @@ def is_modern_cli(argv: list[str]) -> bool: return False first = argv[0].lower() - return first in {"list", "variables", "read", "-h", "--help"} or first in MODERN_COMMAND_ALIASES + return first in {"list", "read", "-h", "--help"} or first in MODERN_COMMAND_ALIASES def normalized_modern_args(argv: list[str]) -> list[str]: @@ -207,7 +206,7 @@ def build_modern_parser() -> argparse.ArgumentParser: ) subparsers = parser.add_subparsers(dest="command") - list_parser = subparsers.add_parser("list", aliases=["variables"], help="List variables in an ESO/MTR data dictionary.") + list_parser = subparsers.add_parser("list", help="List variables in an ESO/MTR data dictionary.") list_parser.add_argument("input_file", nargs="?", default="eplusout.eso", help="ESO/MTR file to inspect.") list_parser.add_argument( "-f", @@ -923,7 +922,7 @@ def run_modern_cli(argv: list[str]) -> int: return 0 try: - if args.command in {"list", "variables"}: + if args.command == "list": records = filter_modern_records(records_for_modern_cli(args.input_file), args.frequency, args.search) write_records(records, args.format) return 0 From 6b16ae07b1e6d41223a117d225b6767af6cfaa5d Mon Sep 17 00:00:00 2001 From: Jason DeGraw Date: Wed, 22 Jul 2026 15:16:10 -0400 Subject: [PATCH 7/7] Correct fmt to std in CommandLineInterface --- src/EnergyPlus/CommandLineInterface.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EnergyPlus/CommandLineInterface.cc b/src/EnergyPlus/CommandLineInterface.cc index 5977d2365fb..4a1fc925665 100644 --- a/src/EnergyPlus/CommandLineInterface.cc +++ b/src/EnergyPlus/CommandLineInterface.cc @@ -966,7 +966,7 @@ state.dataStrGlobals->inputFilePath='{:g}', state, "ERROR: Could not find ReadVarsESO program. When calling through C API, make sure to call setEnergyPlusRootDirectory"); } else { - DisplayString(state, fmt::format("ERROR: Could not find ReadVarsESO program under: {}.", + DisplayString(state, std::format("ERROR: Could not find ReadVarsESO program under: {}.", FileSystem::getAbsolutePath(state.dataStrGlobals->exeDirectoryPath))); } return static_cast(ReturnCodes::Failure);