From 2a6acde3c74e82ca6e71ba2bf25f6f2ca7bce2c5 Mon Sep 17 00:00:00 2001 From: Caleb Date: Fri, 22 Nov 2024 11:20:07 -0500 Subject: [PATCH] WIP finalizing core api; consolidating readme; #204, #162, #163, #184, #180 --- .github/workflows/build-and-deploy.yml | 0 .../workflows/build-and-regression-test.yml | 0 .github/workflows/build-and-unit-test.yml | 94 ++++ Build.md | 34 -- README.md | 123 ++++- Readme.txt | 47 -- ci/conftest.py | 63 +++ ci/download_benchmarks.py | 0 ci/requirements.txt | 1 + ci/test_compare_outputs.py | 16 + python/README.md | 20 - python/epaswmm/CMakeLists.txt | 4 +- python/epaswmm/__init__.py | 5 +- python/epaswmm/epaswmm.pyx | 2 +- python/epaswmm/output/CMakeLists.txt | 15 +- python/epaswmm/output/__init__.py | 2 + python/epaswmm/output/output.pxd | 3 + python/epaswmm/output/output.pyx | 7 +- python/epaswmm/solver/CMakeLists.txt | 20 +- python/epaswmm/solver/__init__.py | 3 +- python/epaswmm/solver/solver.pxd | 25 +- python/epaswmm/solver/solver.pyx | 466 ++++++++++++++---- python/pyproject.toml | 5 +- python/setup.py | 9 + python/tests/test_swmm_solver.py | 36 +- python/tests/test_swwm_output.py | 18 + src/solver/consts.h | 6 +- src/solver/include/swmm5.h | 5 + src/solver/swmm5.c | 78 +++ tests/Unit_Testing.md | 35 -- tests/solver/CMakeLists.txt | 2 +- 31 files changed, 873 insertions(+), 271 deletions(-) create mode 100644 .github/workflows/build-and-deploy.yml create mode 100644 .github/workflows/build-and-regression-test.yml create mode 100644 .github/workflows/build-and-unit-test.yml delete mode 100644 Build.md delete mode 100644 Readme.txt create mode 100644 ci/conftest.py create mode 100644 ci/download_benchmarks.py create mode 100644 ci/requirements.txt create mode 100644 ci/test_compare_outputs.py delete mode 100644 python/README.md delete mode 100644 tests/Unit_Testing.md diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml new file mode 100644 index 000000000..e69de29bb diff --git a/.github/workflows/build-and-regression-test.yml b/.github/workflows/build-and-regression-test.yml new file mode 100644 index 000000000..e69de29bb diff --git a/.github/workflows/build-and-unit-test.yml b/.github/workflows/build-and-unit-test.yml new file mode 100644 index 000000000..649b9c4f2 --- /dev/null +++ b/.github/workflows/build-and-unit-test.yml @@ -0,0 +1,94 @@ +name: Build and Unit Test + +on: + push: + branches: [ master, develop, release ] + # pull_request: + # branches: [ master, develop, release ] + +env: + OMP_NUM_THREADS: 1 + BUILD_HOME: build + TEST_HOME: nrtests + PACKAGE_NAME: vcpkg-export-20220826-200052.1.0.0 + PKG_NAME: vcpkg-export-20220826-200052 + +jobs: + unit_test: + name: Build and unit test + runs-on: windows-2019 + environment: testing + defaults: + run: + shell: cmd + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Install boost-test + env: + REMOTE_STORE: "https://nuget.pkg.github.com/michaeltryby/index.json" + USERNAME: michaeltryby + run: | + nuget sources add -Name github -Source ${{ env.REMOTE_STORE }} -Username ${{ env.USERNAME }} -Password ${{ secrets.ACCESS_TOKEN }} + nuget install ${{env.PKG_NAME}} -Source github + + - name: Build + env: + TOOL_CHAIN_PATH: \scripts\buildsystems\vcpkg.cmake + run: | + cmake -B .\build -DBUILD_TESTS=ON -DCMAKE_TOOLCHAIN_FILE=.\${{env.PACKAGE_NAME}}${{env.TOOL_CHAIN_PATH}} . + cmake --build .\build --config DEBUG + + - name: Unit Test + run: ctest --test-dir .\build -C Debug --output-on-failure + + + reg_test: + name: Build and reg test + runs-on: windows-2019 + defaults: + run: + shell: cmd + working-directory: ci-tools/windows + + steps: + - name: Checkout swmm repo + uses: actions/checkout@v3 + + - name: Checkout ci-tools repo + uses: actions/checkout@v3 + with: + repository: michaeltryby/ci-tools + ref: master + path: ci-tools + + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install requirements + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-swmm.txt + + - name: Build + run: make.cmd /g "Visual Studio 16 2019" + + - name: Before reg test + env: + NRTESTS_URL: https://github.com/USEPA/swmm-nrtestsuite + BENCHMARK_TAG: v2.5.0-dev + run: before-nrtest.cmd ${{ env.BENCHMARK_TAG }} + + - name: Run reg test + run: run-nrtests.cmd %GITHUB_RUN_ID%_%GITHUB_RUN_NUMBER% + + - name: Upload artifacts + if: ${{ always() }} + uses: actions/upload-artifact@v3 + with: + name: build-test-artifacts + path: upload/*.* diff --git a/Build.md b/Build.md deleted file mode 100644 index 07c56ac9a..000000000 --- a/Build.md +++ /dev/null @@ -1,34 +0,0 @@ - - - -## Building SWMM Locally on Windows - - -### Dependencies - -Before the project can be built the required dependencies must be installed. - -**Summary of Build Dependencies: Windows** - - - Build - - Build Tools for Visual Studio 2017 - - CMake 3.13 - - -### Build - -SWMM can be built using cmake with the following commands. - -``` -cmake -B .\build . -cmake --build .\build --config RELEASE - -``` diff --git a/README.md b/README.md index 3ec669ca4..168772df6 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,123 @@ -ORD Stormwater-Management-Model Solver -================================== - -Stormwater Management Model (SWMM) computational engine +EPA ORD Stormwater Management Model (SWMM) +========================================== +Stormwater Management Model (SWMM) computational engine and output post-processing codebase ## Build Status -[![Build and Test](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml) - -## Disclaimer -The United States Environmental Protection Agency (EPA) GitHub project code is provided on an "as is" basis and the user assumes responsibility for its use. EPA has relinquished control of the information and no longer has responsibility to protect the integrity, confidentiality, or availability of the information. Any reference to specific commercial products, processes, or services by service mark, trademark, manufacturer, or otherwise, does not constitute or imply their endorsement, recommendation or favoring by EPA. The EPA seal and logo shall not be used in any manner to imply endorsement of any commercial product or activity by EPA or the United States Government. - +[![Build and Unit Testing](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml) +[![Build and Regression Testing](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml) +[![Deployment](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml) +[![Documentation](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml/badge.svg?branch=docs)](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml) +[![PythonVersion](https://img.shields.io/pypi/pyversions/epaswmm.svg)](https://pypi.org/project/epaswmm) +[![PyPi](https://img.shields.io/pypi/v/epaswmm.svg)](https://pypi.org/project/epaswmm) ## Introduction This is the official SWMM source code repository maintained by US EPA Office of Research and Development, Center For Environmental Solutions & Emergency Response, Water Infrastructure Division located in Cincinnati, Ohio. SWMM is a dynamic hydrology-hydraulic water quality simulation model. It is used for single event or long-term (continuous) simulation of runoff quantity and quality from primarily urban areas. SWMM source code is written in the C Programming Language and released in the Public Domain. +## Build Instructions + +The 'src' folder of this repository contains the C source code for +version of Storm Water Management Model's computational +engine. Consult the included 'Roadmap.txt' file for an overview of +the various code modules. The code can be compiled into both a shared +object library and a command line executable. Under Windows, the +library file (swmm5.dll) is used to power SWMM's graphical user +interface. + +Also included is a python interface for the SWMM computational engine and output +post-processing application programming interfaces located in the python folder. + +The 'CMakeLists.txt' file is a script used by CMake (https://cmake.org/) +to build the SWMM binaries. CMake is a cross-platform build tool +that generates platform native build systems for many compilers. To +check if the required version is installed on your system, enter from +a console window and check that the version is 3.5 or higher. + +```bash +cmake --version +``` + +To build the SWMM engine library and its command line executable +using CMake and the Microsoft Visual Studio C compiler on Windows: + +1. Open a console window and navigate to the directory where this + Readme file resides (which should have 'src' as a sub-directory + underneath it). + +2. Issue the following commands: + +```bash +mkdir build +cd build +``` + +3. Then enter the following CMake commands: + +``` bash +cmake -G .. -A +cmake --build . --config Release +``` + +where `` is the name of the Visual Studio compiler being used +in double quotes (e.g., "Visual Studio 15 2017", "Visual Studio 16 2019", +or "Visual Studio 17 2022") and `` is Win32 for a 32-bit build +or x64 for a 64-bit build. The resulting engine DLL (swmm5.dll), command +line executable (runswmm.exe), and output processing libraries (swmm-output.dll) +will appear in the build\Release directory. + +For other platforms, such as Linux or MacOS, Step 3 can be replaced with: + +```bash +cmake .. +cmake --build . +``` + +The resulting shared object library (libswmm5.so or libswmm5.dylib) and +command line executable (runswmm) will appear in the build directory. + +The exprimental python bindings can be built and installed locally using the following command. + +```bash +cd python +python -m pip install -r requirements.txt +python -m pip install . +``` +Users may also build python wheels for installation or distribution. Once the python bindings +have been validated and cleared through EPA'S clearance process, they will be available for installation +via ropsitories such as pypi. + +## Unit and Regression Testing + +Unit tests and regression tests have been developed for both the natively compiled SWMM computational engine and output toolkit as +well as their respective python bindings. Unit tests for the natively compiled toolkits use the Boost 1.67.0 library and can be +compiled by adding DBUILD_TESTS=ON flag during the cmake build phase as shown below: + +```bash +ctest --test-dir . -DBUILD_TESTS=ON --config Debug --output-on-failure +``` + +Unit testing on the python bindings may be executed using the following command after installation. + +```bash +cd python\tests +pytest . +``` + +Regression tests are executed using the python bindings using the pytest and pytest-regressions extension using the following commands. + +```bash +cd ci +pytest --data-dir --atol --rtol --benchmark-compare --benchmark-json=PATH +``` + ## Find Out More -The source code distributed here is identical to the code found at the official [SWMM Website](https://www.epa.gov/water-research/storm-water-management-model-swmm). +The source code distributed here is identical to the code found at the official [SWMM website](https://www.epa.gov/water-research/storm-water-management-model-swmm). +The SWMM website also hosts the official manuals and installation binaries for the SWMM software. + +A live web version of the SWMM documentation of the API and user manuals can be found on the [SWMM GitHub Pages website](https://usepa.github.io/Stormwater-Management-Model). Note that this is an alpha version that is still under development and has yet to go through EPA'S official QAQC review process. + +## Disclaimer +The United States Environmental Protection Agency (EPA) GitHub project code is provided on an "as is" basis and the user assumes responsibility for its use. EPA has relinquished control of the information and no longer has responsibility to protect the integrity, confidentiality, or availability of the information. Any reference to specific commercial products, processes, or services by service mark, trademark, manufacturer, or otherwise, does not constitute or imply their endorsement, recommendation or favoring by EPA. The EPA seal and logo shall not be used in any manner to imply endorsement of any commercial product or activity by EPA or the United States Government. + diff --git a/Readme.txt b/Readme.txt deleted file mode 100644 index 7bbb52e66..000000000 --- a/Readme.txt +++ /dev/null @@ -1,47 +0,0 @@ -CONTENTS OF SWMM522_ENGINE.ZIP -============================== - -The 'src' folder of this archive contains the C source code for -version 5.2.2 of the Storm Water Management Model's computational -engine. Consult the included 'Roadmap.txt' file for an overview of -the various code modules. The code can be compiled into both a shared -object library and a command line executable. Under Windows, the -library file (swmm5.dll) is used to power SWMM's graphical user -interface. - -The 'CMakeLists.txt' file is a script used by CMake (https://cmake.org/) -to build the SWMM 5.2 binaries. CMake is a cross-platform build tool -that generates platform native build systems for many compilers. To -check if the required version is installed on your system, enter - - cmake --version - -from a console window and check that the version is 3.5 or higher. - -To build the SWMM 5.2 engine library and its command line executable -using CMake and the Microsoft Visual Studio C compiler on Windows: - -1. Open a console window and navigate to the directory where this - Readme file resides (which should have 'src' as a sub-directory - underneath it). - -2. Issue the following commands: - mkdir build - cd build - -3. Then enter the following CMake commands: - cmake -G .. -A - cmake --build . --config Release - -where is the name of the Visual Studio compiler being used -in double quotes (e.g., "Visual Studio 15 2017" or "Visual Studio 16 2019") -and is Win32 for a 32-bit build or x64 for a 64-bit build. -The resulting engine DLL (swmm5.dll) and command line executable -(runswmm.exe) will appear in the build\Release directory. - -For other platforms, such as Linux or MacOS, Step 3 can be replaced with: - cmake .. - cmake --build . - -The resulting shared object library (libswmm5.so) and command line executable -(runswmm) will appear in the build directory. \ No newline at end of file diff --git a/ci/conftest.py b/ci/conftest.py new file mode 100644 index 000000000..223a52989 --- /dev/null +++ b/ci/conftest.py @@ -0,0 +1,63 @@ +# conftest.py +import pytest +import os + +def pytest_addoption(parser): + parser.addoption( + "--data-dir", action="store", default=None, help="Directory to search for data files", required=True + ) + + parser.addoption( + "--atol", action="store", default=None, help="Absolute tolerance for floating point comparisons" + ) + + parser.addoption( + "--rtol", action="store", default=1.0e-8, help="Relative tolerance for floating point comparisons" + ) + + parser.addoption( + "--force-regen", action="store_true", default=1.0e-8, help="Force regeneration of the data files" + ) + +@pytest.fixture +def data_dir(request): + """ + Fixture to get the data directory + """ + return request.config.getoption("--data_dir") + +@pytest.fixture +def atol(request): + """ + Fixture to get the absolute tolerance + """ + return request.config.getoption("--atol") + +@pytest.fixture +def rtol(request): + """ + Fixture to get the relative tolerance + """ + return request.config.getoption("--rtol") + +@pytest.fixture +def force_regen(request): + """ + Fixture to get the force-regen flag + """ + return request.config.getoption("--force_regen") + +@pytest.fixture +def discovered_files(data_dir): + """ + Walk through data directory and discover all SWMM input files + """ + if data_dir is None: + return [] + return [os.path.join(data_dir, f) for f in os.listdir(data_dir) if os.path.isfile(os.path.join(data_dir, f))] + +def pytest_collection_modifyitems(items): + for item in items: + if item.originalname == 'test_compare_node_results' and 'input_file' in item.fixturenames: + input_file = item.callspec.params['input_file'] + item._nodeid = f'{item.nodeid}_{os.path.basename(input_file)}' \ No newline at end of file diff --git a/ci/download_benchmarks.py b/ci/download_benchmarks.py new file mode 100644 index 000000000..e69de29bb diff --git a/ci/requirements.txt b/ci/requirements.txt new file mode 100644 index 000000000..0b6ebdaf5 --- /dev/null +++ b/ci/requirements.txt @@ -0,0 +1 @@ +pytest-regressions \ No newline at end of file diff --git a/ci/test_compare_outputs.py b/ci/test_compare_outputs.py new file mode 100644 index 000000000..e446f687b --- /dev/null +++ b/ci/test_compare_outputs.py @@ -0,0 +1,16 @@ +import pytest +from epaswmm.output import Output + +@pytest.mark.parametrize('input_file', discovered_files) +@pytest.mark.benchmark(group='compare_node_results') +def test_compare_node_results_(benchmark, data_regression, input_file): + """ + Compare the results of the node results and benchmark the execution time. + """ + @benchmark + def run_test(): + # Your test logic here + assert True + + # Optionally, you can add assertions to check the benchmark results + assert benchmark.stats.mean < 0.1 # Example assertion \ No newline at end of file diff --git a/python/README.md b/python/README.md deleted file mode 100644 index 5b49c6f5e..000000000 --- a/python/README.md +++ /dev/null @@ -1,20 +0,0 @@ -Stormwater-Management-Model Python Bindings -================================== - -A python package for Stormwater Management Model (SWMM). - - -## Build Status -[![Build and Test](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/USEPA/Stormwater-Management-Model/actions/workflows/build-and-test.yml) - -## Disclaimer -The United States Environmental Protection Agency (EPA) GitHub project code is provided on an "as is" basis and the user assumes responsibility for its use. EPA has relinquished control of the information and no longer has responsibility to protect the integrity, confidentiality, or availability of the information. Any reference to specific commercial products, processes, or services by service mark, trademark, manufacturer, or otherwise, does not constitute or imply their endorsement, recommendation or favoring by EPA. The EPA seal and logo shall not be used in any manner to imply endorsement of any commercial product or activity by EPA or the United States Government. - - -## Introduction -This is the official SWMM source code repository maintained by US EPA Office of Research and Development, Center For Environmental Solutions & Emergency Response, Water Infrastructure Division located in Cincinnati, Ohio. - -SWMM is a dynamic hydrology-hydraulic water quality simulation model. It is used for single event or long-term (continuous) simulation of runoff quantity and quality from primarily urban areas. SWMM source code is written in the C Programming Language and released in the Public Domain. - -## Find Out More -The source code distributed here is identical to the code found at the official [SWMM Website](https://www.epa.gov/water-research/storm-water-management-model-swmm). diff --git a/python/epaswmm/CMakeLists.txt b/python/epaswmm/CMakeLists.txt index 44879cd47..b734af6e5 100644 --- a/python/epaswmm/CMakeLists.txt +++ b/python/epaswmm/CMakeLists.txt @@ -20,5 +20,5 @@ target_include_directories( ) # Add subdirectories -add_subdirectory(output) -add_subdirectory(solver) \ No newline at end of file +add_subdirectory(solver) +add_subdirectory(output) \ No newline at end of file diff --git a/python/epaswmm/__init__.py b/python/epaswmm/__init__.py index d3c6fa598..c588e4be4 100644 --- a/python/epaswmm/__init__.py +++ b/python/epaswmm/__init__.py @@ -1,4 +1 @@ -from .epaswmm import ( - encode_swmm_datetime, - decode_swmm_datetime, -) +from .epaswmm import * diff --git a/python/epaswmm/epaswmm.pyx b/python/epaswmm/epaswmm.pyx index 896e999ab..d86199500 100644 --- a/python/epaswmm/epaswmm.pyx +++ b/python/epaswmm/epaswmm.pyx @@ -144,7 +144,7 @@ cpdef datetime decode_swmm_datetime(double swmm_datetime): day = d + 1 secs = (int)(math.floor(fracDay + 0.5)) - + if secs >= 86400: secs = 86399 diff --git a/python/epaswmm/output/CMakeLists.txt b/python/epaswmm/output/CMakeLists.txt index 090881394..2abc0c637 100644 --- a/python/epaswmm/output/CMakeLists.txt +++ b/python/epaswmm/output/CMakeLists.txt @@ -3,16 +3,27 @@ # Created by: Caleb Buahin (EPA/ORD/CESER/WID) # Created on: 2024-11-19 +# Find Python +find_package(Python3 REQUIRED COMPONENTS Development) +find_package(PythonExtensions REQUIRED) -add_cython_target(output output.pyx CXX PY3) +add_cython_target(output output.pyx LANGUAGE CXX PY3) + +# Add Cython target add_library(output MODULE ${output}) + +# Add library target_link_libraries(output swmm-output) +# Specify that this is a Python extension module python_extension_module(output) + +# Install the target install(TARGETS output LIBRARY DESTINATION epaswmm/output) +# Include directories target_include_directories( - output PRIVATE + output PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} $ ) diff --git a/python/epaswmm/output/__init__.py b/python/epaswmm/output/__init__.py index d2dd0925c..36e2e1576 100644 --- a/python/epaswmm/output/__init__.py +++ b/python/epaswmm/output/__init__.py @@ -10,4 +10,6 @@ SystemAttribute, SWMMOutputException, Output, + decode_swmm_datetime, + encode_swmm_datetime, ) \ No newline at end of file diff --git a/python/epaswmm/output/output.pxd b/python/epaswmm/output/output.pxd index 53ec48355..e8edf0c0b 100644 --- a/python/epaswmm/output/output.pxd +++ b/python/epaswmm/output/output.pxd @@ -3,9 +3,12 @@ # Created on: 2024-11-19 # cython: language_level=3 +# SWMM datetime encode decoder functions (not very elegant/need to fix later) +# cdef extern from "datetime.h" # SWMM output enumeration types. cdef extern from "swmm_output_enums.h": + # Unit system used in the output file ctypedef enum SMO_unitSystem: SMO_US # US customary units diff --git a/python/epaswmm/output/output.pyx b/python/epaswmm/output/output.pyx index 6f118fd4e..ae39a9fe8 100644 --- a/python/epaswmm/output/output.pyx +++ b/python/epaswmm/output/output.pyx @@ -18,6 +18,11 @@ cimport epaswmm.epaswmm as cepaswmm cimport epaswmm.output.output as coutput from epaswmm.output.output cimport ( + # DateTime, + # datetime_encodeDate, + # datetime_encodeTime, + # datetime_decodeDate, + # datetime_decodeTime, SMO_unitSystem, SMO_flowUnits, SMO_concUnits, @@ -53,7 +58,7 @@ from epaswmm.output.output cimport ( SMO_getSystemResult, SMO_free, SMO_clearError, - SMO_checkError, + SMO_checkError ) class UnitSystem(Enum): diff --git a/python/epaswmm/solver/CMakeLists.txt b/python/epaswmm/solver/CMakeLists.txt index 85d2e1ddd..9c95ce580 100644 --- a/python/epaswmm/solver/CMakeLists.txt +++ b/python/epaswmm/solver/CMakeLists.txt @@ -4,15 +4,31 @@ # Created on: 2024-11-19 # +# Find Python +find_package(Python3 REQUIRED COMPONENTS Development) + +# Find Python extensions +find_package(PythonExtensions REQUIRED) + +# Add Cython target add_cython_target(solver solver.pyx CXX PY3) + +# Add library add_library(solver MODULE ${solver}) -target_link_libraries(solver swmm5) +# Link to SWMM and Python libraries +target_link_libraries(solver swmm5 Python3::Python) + +# Specify that this is a Python extension module python_extension_module(solver) + +# Install the target install(TARGETS solver LIBRARY DESTINATION epaswmm/solver) +# Include directories target_include_directories( solver PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} $ -) + ${Python3_INCLUDE_DIRS} +) \ No newline at end of file diff --git a/python/epaswmm/solver/__init__.py b/python/epaswmm/solver/__init__.py index e6be28f25..07ede387a 100644 --- a/python/epaswmm/solver/__init__.py +++ b/python/epaswmm/solver/__init__.py @@ -14,7 +14,8 @@ SWMMAPIErrors, run_solver, decode_swmm_datetime, + encode_swmm_datetime, version, SolverState, - Solver, + Solver ) diff --git a/python/epaswmm/solver/solver.pxd b/python/epaswmm/solver/solver.pxd index 6ce6dd60f..254479a0b 100644 --- a/python/epaswmm/solver/solver.pxd +++ b/python/epaswmm/solver/solver.pxd @@ -3,6 +3,13 @@ # Created on: 2024-11-19 # cython: language_level=3 +cdef extern from "Python.h": + object PyEval_CallObject(object, object) + +cdef extern from "time.h": + ctypedef long clock_t + clock_t clock() + cdef extern from "swmm5.h": # SWMM object type enumeration ctypedef enum swmm_Object: @@ -162,14 +169,25 @@ cdef extern from "swmm5.h": ERR_API_HOTSTART_FILE_OPEN # Error opening hotstart file ERR_API_HOTSTART_FILE_FORMAT # Invalid hotstart file format + # SWMM API function return simulation progress + ctypedef void (*progress_callback)(double progress); # SWMM API function prototypes # param: inp_file: input file name # param: rpt_file: report file name - # parm: out_file: output file name + # param: out_file: output file name # Returns: error code (0 if successful) cdef int swmm_run(char* inp_file, char* rpt_file, char* out_file) + + # SWMM API function prototypes + # param: inp_file: input file name + # param: rpt_file: report file name + # param: out_file: output file name + # param: progress: progress callback + # Returns: error code (0 if successful) + cdef int swmm_run_with_callback(char* inp_file, char* rpt_file, char* out_file, progress_callback progress) + # Open a SWMM input file # param: inp_file: input file name # param: rpt_file: report file name @@ -278,4 +296,7 @@ cdef extern from "swmm5.h": cdef void swmm_writeLine(const char *line) # Decodes a SWMM datetime into a datetime object - cdef void swmm_decodeDate(double date, int *year, int *month, int *day, int *hour, int *minute, int *second, int *dayOfWeek) \ No newline at end of file + cdef void swmm_decodeDate(double date, int *year, int *month, int *day, int *hour, int *minute, int *second, int *dayOfWeek) + + # Encodes a datetime object into a SWMM datetime + cdef double swmm_encodeDate(int year, int month, int day, int hour, int minute, int second) \ No newline at end of file diff --git a/python/epaswmm/solver/solver.pyx b/python/epaswmm/solver/solver.pyx index a4d0d870e..193f073af 100644 --- a/python/epaswmm/solver/solver.pyx +++ b/python/epaswmm/solver/solver.pyx @@ -6,7 +6,8 @@ # python and cython imports from enum import Enum -from typing import List, Tuple, Union, Optional, Dict, Set +from warnings import warn +from typing import List, Tuple, Union, Optional, Dict, Set, Callable from cpython.datetime cimport datetime, timedelta from libc.stdlib cimport free, malloc @@ -18,7 +19,12 @@ cimport epaswmm.solver.solver as solver cimport epaswmm.epaswmm as cepaswmm # cython: language_level=3 + + from epaswmm.solver.solver cimport ( + PyEval_CallObject, + clock_t, + clock, swmm_Object, swmm_NodeType, swmm_LinkType, @@ -29,7 +35,9 @@ from epaswmm.solver.solver cimport ( swmm_SystemProperty, swmm_FlowUnitsProperty, swmm_API_Errors, + progress_callback, swmm_run, + swmm_run_with_callback, swmm_open, swmm_start, swmm_step, @@ -51,7 +59,8 @@ from epaswmm.solver.solver cimport ( swmm_setValue, swmm_getSavedValue, swmm_writeLine, - swmm_decodeDate + swmm_decodeDate, + swmm_encodeDate ) class SWMMObjects(Enum): @@ -359,21 +368,61 @@ class SWMMAPIErrors(Enum): HOTSTART_FILE_OPEN = swmm_API_Errors.ERR_API_HOTSTART_FILE_OPEN # Error opening hotstart file HOTSTART_FILE_FORMAT = swmm_API_Errors.ERR_API_HOTSTART_FILE_FORMAT # Invalid hotstart file format -cpdef int run_solver(str inp_file, str rpt_file, str out_file): +cdef void c_wrapper_function(double x): + """ + Wrapper function to call a Python function. + + :param x: Input value + :type x: double + """ + global py_progress_callback + cdef tuple args = (x,) + PyEval_CallObject(py_progress_callback, args) + +cdef progress_callback wrap_python_function_as_callback(object py_func): """ - Run a SWMM simulation. + Wrap a Python function as a callback. + + :param py_func: Python function + :type py_func: callable + :return: Callback function + :rtype: progress_callback + """ + global py_progress_callback + py_progress_callback = py_func + return c_wrapper_function + +cdef object global_solver = None + +cdef void progress_callback_wrapper(double progress): + """ + Wrapper function to call the instance method. + :param progress: Progress percentage + """ + global solver_instance + + if solver_instance is not None: + solver_instance.__progress_callback(progress) + +def run_solver(inp_file: str, rpt_file: str, out_file: str, swmm_progress_callback: Callable[[float], None] = None) -> int: + """ + Run a SWMM simulation with a progress callback. + :param inp_file: Input file name :param rpt_file: Report file name :param out_file: Output file name + :param progress_callback: Progress callback function + :type progress_callback: callable :return: Error code (0 if successful) """ cdef int error_code = 0 cdef bytes c_inp_file_bytes = inp_file.encode('utf-8') + cdef progress_callback c_swm_progress_callback if rpt_file is not None: rpt_file = inp_file.replace('.inp', '.rpt') - + if out_file is not None: out_file = inp_file.replace('.inp', '.out') @@ -384,11 +433,15 @@ cpdef int run_solver(str inp_file, str rpt_file, str out_file): cdef const char* c_rpt_file = c_rpt_file_bytes cdef const char* c_out_file = c_out_file_bytes - error_code = swmm_open(c_inp_file, c_rpt_file, c_out_file) + if swmm_progress_callback is not None: + c_swm_progress_callback = wrap_python_function_as_callback(swmm_progress_callback) + error_code = swmm_run_with_callback(c_inp_file, c_rpt_file, c_out_file, c_swm_progress_callback) + else: + error_code = swmm_run(c_inp_file, c_rpt_file, c_out_file) if error_code != 0: - raise Exception(f'Run failed with message: {get_error_message(error_code)}') - + raise SWMMSolverException(f'Run failed with message: {get_error_message(error_code)}') + return error_code cpdef datetime decode_swmm_datetime(double swmm_datetime): @@ -405,6 +458,24 @@ cpdef datetime decode_swmm_datetime(double swmm_datetime): return datetime(year, month, day, hour, minute, second) +cpdef double encode_swmm_datetime(datetime dt): + """ + Encode a datetime object into a SWMM datetime float value. + + :param dt: datetime object + :type dt: datetime + :return: SWMM datetime float value + :rtype: float + """ + cdef int year = dt.year + cdef int month = dt.month + cdef int day = dt.day + cdef int hour = dt.hour + cdef int minute = dt.minute + cdef int second = dt.second + + return swmm_encodeDate(year, month, day, hour, minute, second) + cpdef int version(): """ Get the SWMM version. @@ -465,6 +536,19 @@ class CallbackType(Enum): BEFORE_CLOSE = 11 AFTER_CLOSE = 12 +class SWMMSolverException(Exception): + """ + Exception class for SWMM output file processing errors. + """ + def __init__(self, message: str) -> None: + """ + Constructor to initialize the exception message. + + :param message: Error message. + :type message: str + """ + super().__init__(message) + cdef class Solver: """ A class to represent a SWMM solver. @@ -474,18 +558,11 @@ cdef class Solver: cdef str _out_file cdef bint _save_results cdef int _stride_step - cdef list _before_initialize_callbacks - cdef list _before_open_callbacks - cdef list _after_open_callbacks - cdef list _before_start_callbacks - cdef list _after_start_callbacks - cdef list _before_step_callbacks - cdef list _before_end_callbacks - cdef list _after_end_callbacks - cdef list _before_report_callbacks - cdef list _after_report_callbacks - cdef list _before_close_callbacks - cdef list _after_close_callbacks + cdef dict _callbacks + cdef int _progress_callbacks_per_second + cdef list _progress_callbacks + cdef clock_t _clock + cdef double _total_duration def __cinit__(self, str inp_file, str rpt_file, str out_file, bint save_results=True): """ @@ -495,8 +572,12 @@ cdef class Solver: :param rpt_file: Report file name :param out_file: Output file name """ + global global_solver self._save_results = save_results self._inp_file = inp_file + self._progress_callbacks_per_second = 2 + self._clock = clock() + global_solver = self if rpt_file is not None: self._rpt_file = rpt_file @@ -507,40 +588,40 @@ cdef class Solver: self._out_file = out_file else: self._out_file = inp_file.replace('.inp', '.out') - + + self._stride_step = 0 + + self._callbacks = { + CallbackType.BEFORE_INITIALIZE: [], + CallbackType.BEFORE_OPEN: [], + CallbackType.AFTER_OPEN: [], + CallbackType.BEFORE_START: [], + CallbackType.AFTER_START: [], + CallbackType.BEFORE_STEP: [], + CallbackType.AFTER_STEP: [], + CallbackType.BEFORE_END: [], + CallbackType.AFTER_END: [], + } self._solver_state = SolverState.CREATED def __enter__(self): """ Enter method for context manager. """ - if ( - (self._solver_state != SolverState.CREATED) or - (self._solver_state != SolverState.CLOSED) - ): - raise Exception('') - else: - self.initialize() - + return self def __exit__(self, exc_type, exc_value, traceback): """ Exit method for context manager. """ - self.__close() - - def __close(self): - """ - Close the solver. - """ - pass + self.finalize() def __dealloc__(self): """ Destructor to free the solver. """ - pass + self.finalize() def __iter__(self): """ @@ -552,28 +633,60 @@ cdef class Solver: """ Next method for the solver. """ - pass + if self._solver_state == SolverState.FINISHED: + raise StopIteration + else: + return self.step() @property - def start_date(self) -> datetime: + def start_datetime(self) -> datetime: """ Get the start date of the simulation. :return: Start date :rtype: datetime """ - pass + cdef double start_date = swmm_getValue(SWMMSystemProperties.START_DATE.value, 0) + return cepaswmm.decode_swmm_datetime(start_date) + + @start_datetime.setter + def start_datetime(self, sim_start_datetime: datetime) -> None: + """ + Initialize the solver. + + :param sim_start_datetime: Start date of the simulation + :return: Error code (0 if successful) + """ + cdef double start_date = cepaswmm.encode_swmm_datetime(sim_start_datetime) + cdef int error_code = swmm_setValue(SWMMSystemProperties.START_DATE.value, 0, start_date) + + self.__validate_error(error_code) + @property - def end_date(self) -> datetime: + def end_datetime(self) -> datetime: """ Get the end date of the simulation. :return: End date :rtype: datetime """ - pass - + cdef double end_date = swmm_getValue(SWMMSystemProperties.END_DATE.value, 0) + return cepaswmm.decode_swmm_datetime(end_date) + + @property.setter + def end_datetime(self, sim_end_datetime: datetime) -> None: + """ + Set the end date of the simulation. + + :param sim_end_datetime: End date of the simulation + :return: Error code (0 if successful) + """ + cdef double end_date = cepaswmm.encode_swmm_datetime(sim_end_datetime) + cdef int error_code = swmm_setValue(SWMMSystemProperties.END_DATE.value, 0, end_date) + + self.__validate_error(error_code) + @property def current_date(self) -> datetime: """ @@ -582,7 +695,32 @@ cdef class Solver: :return: Current date :rtype: datetime """ - pass + cdef double current_date = swmm_getValue(SWMMSystemProperties.CURRENT_DATE.value, 0) + return cepaswmm.decode_swmm_datetime(current_date) + + def set_value(self, property_type: SWMMObjects, index: int, value: double) -> None: + """ + Set a SWMM system property value. + + :param property_type: System property type + :type property_type: SWMMSystemProperties + :param value: Property value + :type value: double + """ + cdef int error_code = swmm_setValue(property_type.value, index, value) + self.__validate_error(error_code) + + def get_value(self, property_type: SWMMObjects, index: int): + """ + Get a SWMM system property value. + + :param property_type: System property type + :type property_type: SWMMSystemProperties + :return: Property value + :rtype: double + """ + cdef double value = swmm_getValue(property_type.value, index) + return value @property def stride_step(self) -> int: @@ -604,52 +742,125 @@ cdef class Solver: """ pass - def __validate_error(self, error_code: int) -> None: + def add_callback(self, callback_type: CallbackType, callback: Callable[[Solver], None]) -> None: """ - Validate the error code and raise an exception if it is not 0. + Add a callback to the solver. - :param error_code: Error code to validate - :type error_code: int + :param callback_type: Type of callback + :type callback_type: CallbackType + :param callback: Callback function + :type callback: callable """ - if error_code != 0: - raise Exception(f'Run failed with message: {self.__get_error()}') - - def status(self) -> SolverState: + self._callbacks[callback_type].append(callback) + + def add_progress_callback(self, callback: Callable[[double], None]) -> None: """ - Get the status of the solver. + Add a progress callback to the solver. - :return: Solver state - :rtype: SolverState + :param callback: Progress callback function + :type callback: callable """ - pass + self._progress_callbacks.append(callback) - cpdef str __get_error(self): + cpdef void initialize(self): """ - Get the error code from the solver. + Initialize the solver. - :return: Error code - :rtype: int + :param inp_file: Input file name + :param rpt_file: Report file name + :param out_file: Output file name + """ - cdef char* c_error_message = malloc(1024*sizeof(char)) - swmm_getError(c_error_message, 1024) + cdef error_code = 0 + self._clock = clock() - error_message = c_error_message.decode('utf-8') + cdef bytes c_inp_file_bytes = self._inp_file.encode('utf-8') + cdef bytes c_rpt_file_bytes = self._rpt_file.encode('utf-8') + cdef bytes c_out_file_bytes = self._out_file.encode('utf-8') - free(c_error_message) + cdef const char* c_inp_file = c_inp_file_bytes + cdef const char* c_rpt_file = c_rpt_file_bytes + cdef const char* c_out_file = c_out_file_bytes - return error_message + if ( + (self._solver_state != SolverState.CREATED) or + (self._solver_state != SolverState.CLOSED) + ): + raise SWMMSolverException(f'Initialize failed: Solver is not in a valid state: {self._solver_state}') + else: - def __initialize(self) -> int: + self.__execute_callbacks(CallbackType.BEFORE_INITIALIZE) + self.__execute_callbacks(CallbackType.BEFORE_OPEN) + error_code = swmm_open(c_inp_file, c_rpt_file, c_out_file) + self.__validate_error(error_code) + self._solver_state = SolverState.OPEN + self.__execute_callbacks(CallbackType.AFTER_OPEN) + + self.__execute_callbacks(CallbackType.BEFORE_START) + error_code = swmm_start(self._save_results) + self.__validate_error(error_code) + self._solver_state = SolverState.STARTED + self.__execute_callbacks(CallbackType.AFTER_START) + + self._total_duration = swmm_getValue(SWMMSystemProperties.END_DATE.value, 0) - swmm_getValue(SWMMSystemProperties.START_DATE.value, 0) + + cpdef double step(self): """ - Open a SWMM input file. + Step a SWMM simulation. - :param inp_file: Input file name - :param rpt_file: Report file name - :param out_file: Output file name :return: Error code (0 if successful) """ - cdef error_code = 0 + cdef double elapsed_time = 0.0 + cdef double progress = 0.0 + + if self._stride_step > 0: + error_code = swmm_stride(self._stride_step, &elapsed_time) + else: + error_code = swmm_step(&elapsed_time) + self.__validate_error(error_code) + + progress = (swmm_getValue(SWMMSystemProperties.CURRENT_DATE.value, 0) - self._total_duration) / self._total_duration + self.__execute_progress_callbacks(progress) + + if elapsed_time <= 0.0: + self._solver_state = SolverState.FINISHED + + return elapsed_time + + cpdef void finalize(self): + """ + Finalize the solver. + """ + cdef int error_code = 0 + + if self._solver_state == SolverState.OPEN or self._solver_state == SolverState.STARTED or self._solver_state == SolverState.FINISHED: + self.__execute_callbacks(CallbackType.BEFORE_END) + error_code = self.swmm_end() + self.__validate_error(error_code) + self._solver_state = SolverState.ENDED + self.__execute_callbacks(CallbackType.AFTER_END) + + self.__execute_callbacks(CallbackType.BEFORE_REPORT) + error_code = self.swmm_report() + self.__validate_error(error_code) + self._solver_state = SolverState.REPORTED + self.__execute_callbacks(CallbackType.AFTER_REPORT) + + self.__execute_callbacks(CallbackType.BEFORE_CLOSE) + error_code = self.swmm_close() + self.__validate_error(error_code) + self._solver_state = SolverState.CLOSED + self.__execute_callbacks(CallbackType.AFTER_CLOSE) + + cpdef void execute(self): + """ + Run the solver to completion. + + :return: Error code (0 if successful) + """ + cdef int error_code = 0 + cdef progress_callback swmm_progress_callback = progress_callback_wrapper cdef bytes c_inp_file_bytes = self._inp_file.encode('utf-8') cdef bytes c_rpt_file_bytes = self._rpt_file.encode('utf-8') cdef bytes c_out_file_bytes = self._out_file.encode('utf-8') @@ -658,60 +869,111 @@ cdef class Solver: cdef const char* c_rpt_file = c_rpt_file_bytes cdef const char* c_out_file = c_out_file_bytes - error_code = swmm_open(c_inp_file, c_rpt_file, c_out_file) + if ( + (self._solver_state != SolverState.CREATED) or + (self._solver_state != SolverState.CLOSED) + ): + raise SWMMSolverException(f'Solver is not in a valid state: {self._solver_state}') + else: + if len(self.__execute_progress_callbacks) > 0: + error_code = swmm_run_with_callback(c_inp_file, c_rpt_file, c_out_file, swmm_progress_callback) + else: + error_code = swmm_run(c_inp_file, c_rpt_file, c_out_file) + + cpdef void use_hotstart(self, str hotstart_file): + """ + Use a hotstart file. + + :param hotstart_file: Hotstart file name + """ + cdef bytes c_hotstart_file = hotstart_file.encode('utf-8') + cdef const char* cc_hotstart_file = c_hotstart_file + cdef int error_code = swmm_useHotStart(cc_hotstart_file) + + self.__validate_error(error_code) + + cpdef void save_hotstart(self, str hotstart_file): + """ + Save a hotstart file. + + :param hotstart_file: Hotstart file name + """ + cdef bytes c_hotstart_file = hotstart_file.encode('utf-8') + cdef const char* cc_hotstart_file = c_hotstart_file + cdef int error_code = swmm_saveHotStart(cc_hotstart_file) + self.__validate_error(error_code) - def __finalize(self) -> int: + def get_mass_balance_error(self) -> Tuple[float, float, float]: """ - Close a SWMM input file. + Get the mass balance error. - :return: Error code (0 if successful) + :return: Mass balance error + :rtype: Tuple[float, float, float] """ - cdef error_code = 0 + cdef int error_code = 0 + cdef float runoffErr, flowErr, qualErr - error_code = swmm_close() + swmm_getMassBalErr(&runoffErr, &flowErr, &qualErr) self.__validate_error(error_code) - def __start(self, save_hotstart: bool) -> int: + def __execute_callbacks(self, callback_type: CallbackType) -> None: """ - Start a SWMM simulation. + Execute the callbacks for the given type. - :param save_hotstart: Flag to save hotstart file - :return: Error code (0 if successful) + :param callback_type: Type of callback + :type callback_type: CallbackType """ - pass + for callback in self._callbacks[callback_type]: + callback(self) - def step(self) -> datetime: + cpdef void __execute_progress_callbacks(self, double percent_complete): """ - Step a SWMM simulation. + Execute the progress callbacks. - :return: Error code (0 if successful) + :param percent_complete: Percent complete + :type percent_complete: float """ - pass + for callback in self._progress_callbacks: + callback(percent_complete) - def step_stride(self, stride: int) -> int: + cdef void __progress_callback(self, double percent_complete): """ - Stride a SWMM simulation. + Progress callback for the solver. - :param stride: Number of steps to stride - :return: Error code (0 if successful) + :param percent_complete: Percent complete + :type percent_complete: float """ - pass + cdef clock_t elapsed_time = clock() - self._clock + + if elapsed_time > 1.0 / self._progress_callbacks_per_second: + self.__execute_progress_callbacks(percent_complete) + self._clock = clock() - def use_hotstart(self, hotstart_file: str) -> int: + cpdef void __validate_error(self, error_code: int) : """ - Use a hotstart file. + Validate the error code and raise an exception if it is not 0. - :param hotstart_file: Hotstart file name - :return: Error code (0 if successful) + :param error_code: Error code to validate + :type error_code: int """ - pass - - def save_hotstart(self, hotstart_file: str) -> int: + if error_code != 0: + raise SWMMSolverException(f'SWMM failed with message: {self.__get_error()}') + + cdef str __get_error(self): """ - Save a hotstart file. + Get the error code from the solver. - :param hotstart_file: Hotstart file name - :return: Error code (0 if successful) + :return: Error code + :rtype: int """ - pass \ No newline at end of file + cdef char* c_error_message = malloc(1024*sizeof(char)) + swmm_getError(c_error_message, 1024) + + error_message = c_error_message.decode('utf-8') + + free(c_error_message) + + return error_message + + diff --git a/python/pyproject.toml b/python/pyproject.toml index 0a35d9c1c..e0b0e737a 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -16,10 +16,11 @@ build-backend = "setuptools.build_meta" [project] name = "epaswmm" description = "A python package for EPA SWMM5 preprocessing, solver, and post-processing." -readme = {file = "README.md",content-type = "text/markdown"} +# readme = {file = "./../README.md", content-type = "text/markdown"} +#specify dynamic readme from setup.py license = { file = "LICENSE" } requires-python = ">=3.8" -dynamic = ["version"] +dynamic = ["version", "readme"] keywords = ["SWMM", "stormwater", "modeling", "water", "hydrology", "hydraulics", "wastewater"] authors = [ diff --git a/python/setup.py b/python/setup.py index 30f8bab0d..832306785 100644 --- a/python/setup.py +++ b/python/setup.py @@ -20,6 +20,13 @@ platform_system = platform.system() +# Get the directory containing this file +here = os.path.abspath(os.path.dirname(__file__)) + +# Read the README file +with open(os.path.join(here, '../README.md'), encoding='utf-8') as f: + long_description = f.read() + def get_version(): """ Get version from toolkit @@ -45,6 +52,8 @@ def get_version(): setup( version=get_version(), + long_description=long_description, + long_description_content_type='text/markdown', packages=[ "epaswmm", ], diff --git a/python/tests/test_swmm_solver.py b/python/tests/test_swmm_solver.py index 88580fa5a..844fa3e0e 100644 --- a/python/tests/test_swmm_solver.py +++ b/python/tests/test_swmm_solver.py @@ -4,6 +4,7 @@ # python imports import unittest +from datetime import datetime # third party imports @@ -17,6 +18,10 @@ class TestSWMMSolver(unittest.TestCase): def setUp(self): pass + @staticmethod + def progress_callback(progress: float) -> None: + assert 0 <= progress <= 1.0 + def test_get_swmm_version(self): """ Test the version function of the SWMM solver @@ -25,6 +30,24 @@ def test_get_swmm_version(self): version = solver.version() self.assertEqual(version, 53000, "SWMM version retrieved successfully") + def test_swmm_encode_date(self): + """ + Test the encode_swmm_datetime function + :return: + """ + + swmm_datetime = datetime(year=2024, month=11, day=16, hour=13, minute=33, second=21) + swmm_datetime_encoded = solver.encode_swmm_datetime(swmm_datetime) + self.assertAlmostEqual(swmm_datetime_encoded, 45612.564826389) + + def test_swmm_decode_date(self): + """ + Test the decode_swmm_datetime function + :return: + """ + swmm_datetime = solver.decode_swmm_datetime(45612.564826389) + self.assertEqual(swmm_datetime, datetime(year=2024, month=11, day=16, hour=13, minute=33, second=21)) + def test_run_solver(self): error = solver.run_solver( inp_file=example_solver_data.SITE_DRAINAGE_EXAMPLE_INPUT_FILE, @@ -34,8 +57,17 @@ def test_run_solver(self): self.assertEqual(error, 0, "SWMM solver run successfully.") - def test_run_solver_invalid_inp_file(self): + def test_run_solver_with_progress_callback(self): + error = solver.run_solver( + inp_file=example_solver_data.SITE_DRAINAGE_EXAMPLE_INPUT_FILE, + rpt_file=example_solver_data.SITE_DRAINAGE_EXAMPLE_INPUT_FILE.replace(".inp", ".rpt"), + out_file=example_solver_data.SITE_DRAINAGE_EXAMPLE_INPUT_FILE.replace(".inp", ".out"), + swmm_progress_callback=self.progress_callback + ) + self.assertEqual(error, 0, "SWMM solver with callbacks run successfully.") + + def test_run_solver_invalid_inp_file(self): with self.assertRaises(Exception) as context: error = solver.run_solver( inp_file=example_solver_data.NON_EXISTENT_INPUT_FILE, @@ -43,4 +75,4 @@ def test_run_solver_invalid_inp_file(self): out_file=example_solver_data.NON_EXISTENT_INPUT_FILE.replace(".inp", ".out"), ) - self.assertIn('ERROR 303: cannot open input file.', str(context.exception)) \ No newline at end of file + self.assertIn('ERROR 303: cannot open input file.', str(context.exception)) diff --git a/python/tests/test_swwm_output.py b/python/tests/test_swwm_output.py index 808d8a805..59873ea51 100644 --- a/python/tests/test_swwm_output.py +++ b/python/tests/test_swwm_output.py @@ -29,6 +29,24 @@ def setUp(self): with open(example_output_data.JSON_TIME_SERIES_FILE, 'rb') as f: self.test_artifacts = pickle.load(f) + # def test_output_encode_date(self): + # """ + # Test the encode_swmm_datetime function + # :return: + # """ + # + # swmm_datetime = datetime(year=2024, month=11, day=16, hour=13, minute=33, second=21) + # swmm_datetime_encoded = output.encode_swmm_datetime(swmm_datetime) + # self.assertAlmostEqual(swmm_datetime_encoded, 45612.564826389) + # + # def test_output_decode_date(self): + # """ + # Test the decode_swmm_datetime function + # :return: + # """ + # swmm_datetime = output.decode_swmm_datetime(45612.564826389) + # self.assertEqual(swmm_datetime, datetime(year=2024, month=11, day=16, hour=13, minute=33, second=21)) + def test_output_unit_system_enum(self): """ Test the output unit system enum diff --git a/src/solver/consts.h b/src/solver/consts.h index 205a3c1d5..a7942a101 100644 --- a/src/solver/consts.h +++ b/src/solver/consts.h @@ -2,15 +2,15 @@ // consts.h // // Project: EPA SWMM5 -// Version: 5.2 -// Date: 07/15/23 (Build 5.2.4) +// Version: 5.3 +// Date: 07/15/23 (Build 5.3.0) // Author: L. Rossman // // Various Constants // // Update history // ============== -// Biuld 5.3.0 +// Build 5.3.0 // - Added MAXHOTSTARTFILES constant to support saving multiple hotstart files // at different times. // diff --git a/src/solver/include/swmm5.h b/src/solver/include/swmm5.h index 9fb41b4cf..4d1a4118f 100644 --- a/src/solver/include/swmm5.h +++ b/src/solver/include/swmm5.h @@ -206,8 +206,10 @@ typedef enum { ERR_API_HOTSTART_FILE_FORMAT= -999911, } swmm_API_Errors; +typedef void (*progress_callback)(double progress); int DLLEXPORT swmm_run(const char *inputFile, const char *reportFile, const char *outputFile); +int DLLEXPORT swmm_run_with_callback(const char *inputFile, const char *reportFile, const char *outputFile, progress_callback callback); int DLLEXPORT swmm_open(const char *inputFile, const char *reportFile, const char *outputFile); int DLLEXPORT swmm_start(int saveFlag); int DLLEXPORT swmm_step(double *elapsedTime); @@ -233,6 +235,9 @@ double DLLEXPORT swmm_getSavedValue(int property, int index, int period); void DLLEXPORT swmm_writeLine(const char *line); void DLLEXPORT swmm_decodeDate(double date, int *year, int *month, int *day, int *hour, int *minute, int *second, int *dayOfWeek); +double DLLEXPORT swmm_encodeDate(int year, int month, int day, + int hour, int minute, int second); + #ifdef __cplusplus } // matches the linkage specification from above */ diff --git a/src/solver/swmm5.c b/src/solver/swmm5.c index ef2301bb6..72ed69103 100644 --- a/src/solver/swmm5.c +++ b/src/solver/swmm5.c @@ -256,6 +256,70 @@ int DLLEXPORT swmm_run(const char* inputFile, const char* reportFile, const char return ErrorCode; } +//============================================================================= + +int DLLEXPORT swmm_run_with_callback(const char* inputFile, const char* reportFile, const char* outputFile, progress_callback callback) +// +// Input: inputFile = name of input file +// reportFile = name of report file +// outputFile = name of binary output file +// callback = function pointer to a progress callback function +// Output: returns error code +// Purpose: runs a SWMM simulation. +// +{ + double progress = 0.0, elapsedTime = 0.0; + + // --- initialize flags + IsOpenFlag = FALSE; + IsStartedFlag = FALSE; + SaveResultsFlag = TRUE; + + // --- open the files & read input data + ErrorCode = 0; + + swmm_open(inputFile, reportFile, outputFile); + + // --- run the simulation if input data OK + if ( !ErrorCode ) + { + // --- initialize values + swmm_start(TRUE); + + // --- execute each time step until elapsed time is re-set to 0 + if ( !ErrorCode ) + { + do + { + swmm_step(&elapsedTime); + + // --- calculate progress + if (callback != NULL) + { + progress = NewRoutingTime / TotalDuration; + callback(progress); + } + + } while ( elapsedTime > 0.0 && !ErrorCode ); + } + + // --- clean up + swmm_end(); + } + + // --- report results + if ( !ErrorCode && Fout.mode == SCRATCH_FILE ) + { + swmm_report(); + } + + // --- close the system + swmm_close(); + + return ErrorCode; +} + + //============================================================================= int DLLEXPORT swmm_open(const char* inputFile, const char* reportFile, const char* outputFile) @@ -1064,6 +1128,20 @@ void DLLEXPORT swmm_decodeDate(double date, int *year, int *month, int *day, *dayOfWeek = datetime_dayOfWeek(date); } +//============================================================================= + +double DLLEXPORT swmm_encodeDate(int year, int month, int day, + int hour, int minute, int second) +// +// Input: date's year, month of year, day of month, time of day (hour, +// minute, second), and day of weeek +// Output: date = an encoded date in decimal days +// Purpose: retrieves the calendar date and clock time of a decoded date. +{ + return datetime_encodeDate(year, month, day) + datetime_encodeTime(hour, minute, second); +} + + //============================================================================= // Object property getters and setters //============================================================================= diff --git a/tests/Unit_Testing.md b/tests/Unit_Testing.md deleted file mode 100644 index 65869798d..000000000 --- a/tests/Unit_Testing.md +++ /dev/null @@ -1,35 +0,0 @@ - - - -## Unit Testing SWMM locally on Windows - -### Dependencies - -Before the project can be built and tested the required dependencies must be installed. - -**Summary of Build Dependencies: Windows** - - - Build - - Build Tools for Visual Studio 2017 - - CMake 3.13 - - - Unit Test - - Boost 1.67.0 (installed in default location "C:\\local") - - - -### Build and Unit Test - -SWMM can be built and unit tests run with one simple command. -``` -\> cd swmm -\swmm>tools\make.cmd /t -``` diff --git a/tests/solver/CMakeLists.txt b/tests/solver/CMakeLists.txt index a524ab5c3..7c08011e5 100644 --- a/tests/solver/CMakeLists.txt +++ b/tests/solver/CMakeLists.txt @@ -9,7 +9,7 @@ # Test solver api add_executable( - test_solver_api + test_solver test_solver_api.cpp )