Add Python bindings to Btwxt via SWIG - #44
Conversation
Note: for MSVC, UseSWIG.cmake already sets it to .pyd
(Tired of windows/MSVC...)
| python_bindings: true | ||
| arch: arm64 | ||
| - os: macos | ||
| os_ver: "11" |
There was a problem hiding this comment.
macos-11 was removed from GHA a long time ago.
| arch: x86_64 | ||
| - os: macos | ||
| os_ver: "12" | ||
| os_ver: "14" |
There was a problem hiding this comment.
There was a problem hiding this comment.
| cmake_policy(SET CMP0078 NEW) | ||
| cmake_policy(SET CMP0086 NEW) |
There was a problem hiding this comment.
Couple of SWIG-related policies
| add_subdirectory("include/${PROJECT_NAME}") | ||
| add_subdirectory(src) | ||
| add_subdirectory(vendor) | ||
| add_subdirectory(python) |
There was a problem hiding this comment.
Add subdir. Did not change the other directories.
| add_custom_target(python_package | ||
| COMMAND ${CMAKE_COMMAND} -E rm -Rf "build/" "dist/" "btwxt.egg-info/" | ||
| COMMAND ${CMAKE_COMMAND} -E echo "To create the package, cd 'python/'" | ||
| COMMAND ${CMAKE_COMMAND} -E echo "${Python_EXECUTABLE} setup.py bdist_wheel" | ||
| COMMAND ${Python_EXECUTABLE} setup.py bdist_wheel |
There was a problem hiding this comment.
Build the python wheel (.whl) with this custom target
| COMMAND ${CMAKE_COMMAND} -E echo "To upload the package, cd 'python/'" | ||
| COMMAND ${CMAKE_COMMAND} -E echo "${Python_EXECUTABLE} -m twine upload --verbose --skip-existing --repository ${PYTHON_PIP_REPOSITORY} dist/*" |
There was a problem hiding this comment.
Do not upload, but tells you how
| if (${PROJECT_NAME}_BUILD_TESTING) | ||
| set(test_case_name btwxt_python) | ||
| add_test( | ||
| NAME "${test_case_name}" | ||
| COMMAND ${Python_EXECUTABLE} -m pytest --verbose test_btwxt.py | ||
| WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/test | ||
| ) | ||
| # Pytest Exit code 5 means all tests were deselected | ||
| set_tests_properties("${test_case_name}" PROPERTIES SKIP_RETURN_CODE 5) | ||
| set_tests_properties("${test_case_name}" PROPERTIES ENVIRONMENT "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}/btwxt") | ||
| endif() |
There was a problem hiding this comment.
Register a pytest file as a CTest.
| import btwxt | ||
| import pytest | ||
| from itertools import product | ||
|
|
||
| def test_grid_axis(): | ||
| axis_x = btwxt.GridAxis() | ||
| assert axis_x.get_length() == 0 | ||
|
|
||
| xs = [30, 35, 40, 45, 50, 55] | ||
| axis_x = btwxt.GridAxis( | ||
| [30, 35, 40, 45, 50, 55], # Values | ||
| btwxt.InterpolationMethod_cubic, # interpolation method | ||
| btwxt.ExtrapolationMethod_linear, # extrapolation_method | ||
| (20.0, 60.0), # extrapolation_limits | ||
| "LWT", # name | ||
| ) | ||
| assert axis_x.get_length() == 6 | ||
| assert set(axis_x.get_values()) == set(xs) | ||
| assert axis_x.get_interpolation_method() == btwxt.InterpolationMethod_cubic | ||
| assert axis_x.get_extrapolation_method() == btwxt.ExtrapolationMethod_linear | ||
| assert axis_x.get_extrapolation_limits() == (20.0, 60.0) | ||
|
|
||
|
|
||
| def F(x, y): | ||
| return x**2 + y**2 | ||
|
|
||
| def test_interpolator(): | ||
| x0 = [1.0, 2.0, 3.0] | ||
| x1 = [10.0, 20.0] | ||
| # Not using numpy on purpose | ||
| ys = [F(x,y) for (x, y) in product(x0, x1)] | ||
|
|
||
| axis_x0 = btwxt.GridAxis( | ||
| x0, # Values | ||
| btwxt.InterpolationMethod_cubic, # interpolation method | ||
| btwxt.ExtrapolationMethod_linear, # extrapolation_method | ||
| (0, 5), # extrapolation_limits | ||
| "x0", # name | ||
| ) | ||
| axis_x1 = btwxt.GridAxis( | ||
| x1, # Values | ||
| btwxt.InterpolationMethod_cubic, # interpolation method | ||
| btwxt.ExtrapolationMethod_linear, # extrapolation_method | ||
| (0, 30), # extrapolation_limits | ||
| "x1", # name | ||
| ) | ||
| grid_point_y = btwxt.GridPointDataSet(ys, "y") | ||
|
|
||
| interpolator = btwxt.RegularGridInterpolator([axis_x0, axis_x1], [grid_point_y]) | ||
|
|
||
| target_normalization = [2, 10] | ||
| normalizationDivisor = 1.0 | ||
| normalizationDivisor = interpolator.normalize_grid_point_data_set_at_target( | ||
| 0, target_normalization, normalizationDivisor | ||
| ) * normalizationDivisor | ||
| assert normalizationDivisor == 104.0 | ||
|
|
||
| interp_x0s = [float(x) for x in range(0, 6, 1)] | ||
| interp_oat_dbs = [float(x) for x in range(0, 31, 5)] | ||
|
|
||
| test_points = [(x,y) for (x, y) in product(interp_x0s, interp_oat_dbs)] | ||
| true_values = [F(x, y) for (x, y) in test_points] | ||
| interp_values = [ | ||
| interpolator.get_value_at_target((x0, x1), 0) * normalizationDivisor | ||
| for (x0, x1) in test_points | ||
| ] |
There was a problem hiding this comment.
pytest file to test out the python bindings
| - name: Set up Python 3.12 | ||
| if: ${{ matrix.python_bindings }} | ||
| id: setup-python | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: 3.12 | ||
|
|
||
| - name: Build Python Wheel | ||
| if: ${{ matrix.python_bindings }} | ||
| working-directory: ./build/ | ||
| run: | | ||
| begin_group() { echo -e "::group::\033[93m$1\033[0m"; } | ||
|
|
||
| begin_group "Install other python dependencies" | ||
| python -m pip install --upgrade pip | ||
| # Ubuntu 22.04 from apt will get swig that's too old to support attributes like [[ nodiscard ]], so get from PyPi | ||
| pip install setuptools wheel twine requests packaging pytest swig | ||
| echo -e "::endgroup::" | ||
|
|
||
| begin_group "Build target" | ||
| cmake -DBUILD_PYTHON_BINDINGS:BOOL=ON -DPython_REQUIRED_VERSION:STRING=${{ steps.setup-python.outputs.python-version }} -DPython_ROOT_DIR:PATH=$RUNNER_TOOL_CACHE/Python/${{ steps.setup-python.outputs.python-version }}/${{ matrix.arch }}/ . | ||
| cmake --build . --config ${{ matrix.config }} --target python_package | ||
| echo -e "::endgroup::" | ||
|
|
||
| - name: Python CTest | ||
| if: ${{ matrix.python_bindings }} | ||
| working-directory: ./build/ | ||
| run: | | ||
| ctest -C ${{ matrix.config }} -R python -VV | ||
|
|
||
| - name: Zip the wheel to maintain case sensitivy and file permissions | ||
| if: ${{ matrix.python_bindings }} | ||
| working-directory: ./build/python | ||
| shell: bash | ||
| run: | | ||
| TGZ_STEM="btwxt-python-${{ matrix.os }}-${{ matrix.os_ver }}-${{ matrix.arch }}" | ||
| echo "TGZ_STEM=$TGZ_STEM" >> $GITHUB_ENV | ||
| tar -cvzf "$TGZ_STEM.tar.gz" dist/ | ||
|
|
||
| - name: Upload .whl to artifact | ||
| if: ${{ matrix.python_bindings }} | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: ${{ env.TGZ_STEM }} | ||
| path: "./build/python/${{ env.TGZ_STEM }}.tar.gz" |
There was a problem hiding this comment.
New workflow bits to build the python bindings after the regular build is done.
Runs the python ctest, and uploads the wheel as artifacts
|
@jmarrec, thanks for putting this together! We've always had the intention to provide bindings for our C++ libraries, but we haven't yet settled on the best approach. We also haven't gotten very far in investigating the options, and I really don't know much about SWIG other than seeing some mixed reviews. |
I was struggling to understand what EnergyPlus was doing with my Table:Lookup and Table:IndependentVariables, and wanted a way to calculate and plot what was going on (and compare with
scipy.interpolate.RegularGridInterpolator).Instead of using a debugger, or tweaking E+ source code to spit out the values I needed, I figured it would be much better to add Python bindings.
Since I had done that, I figured it could be useful for other people (at least some E+ devs, and yourselves as btwxt devs for inspection/validation purposes), so I'm proposing it for inclusion.
This PR:
Find a working workflow with artifacts here: https://github.com/jmarrec/btwxt/actions/runs/12986553478