diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 131b648..d680dc8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,11 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: awvwgk/setup-fortran@main + id: setup-fortran + with: + compiler: gcc + version: 11 - uses: wntrblm/nox@2022.11.21 - run: nox diff --git a/noxfile.py b/noxfile.py index 55a3eeb..2001736 100644 --- a/noxfile.py +++ b/noxfile.py @@ -4,7 +4,7 @@ hello_list = ["hello-pure", "hello-cpp", "hello-pybind11", "hello-cython"] if not sys.platform.startswith("win"): - hello_list.append("hello-cmake-package") + hello_list.extend(["hello-cmake-package", "pi-fortran"]) long_hello_list = [*hello_list, "pen2-cython", "core-c-hello", "core-pybind11-hello"] diff --git a/projects/pi-fortran/CMakeLists.txt b/projects/pi-fortran/CMakeLists.txt new file mode 100644 index 0000000..b04d74a --- /dev/null +++ b/projects/pi-fortran/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.17.2...3.26) + +project(pi + VERSION 1.0.1 + DESCRIPTION "pi estimator" + LANGUAGES C Fortran +) + +find_package(Python REQUIRED COMPONENTS Interpreter Development.Module NumPy) + +# F2PY headers +execute_process( + COMMAND "${Python_EXECUTABLE}" + -c "import numpy.f2py; print(numpy.f2py.get_include())" + OUTPUT_VARIABLE F2PY_INCLUDE_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +set(f2py_module_name "pi") +set(fortran_src_file "${CMAKE_SOURCE_DIR}/pi/_pi.f") +set(f2py_module_c "${f2py_module_name}module.c") + +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_c}" + COMMAND ${PYTHON_EXECUTABLE} -m "numpy.f2py" + "${fortran_src_file}" + "${CMAKE_SOURCE_DIR}/pi/pi.pyf" # Must include custom .pyf file + -m "pi" + --lower # Important + DEPENDS pi/_pi.f # Fortran source +) + +python_add_library(${CMAKE_PROJECT_NAME} MODULE + "${f2py_module_name}module.c" + "${F2PY_INCLUDE_DIR}/fortranobject.c" + "${fortran_src_file}" WITH_SOABI) + +target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC + ${F2PY_INCLUDE_DIR} + ) + +target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Python::NumPy) + +install(TARGETS ${CMAKE_PROJECT_NAME} DESTINATION pi) diff --git a/projects/pi-fortran/README.md b/projects/pi-fortran/README.md new file mode 100644 index 0000000..72bb506 --- /dev/null +++ b/projects/pi-fortran/README.md @@ -0,0 +1,25 @@ +# Pi + +This is an example project demonstrating the use of scikit-build for distributing a standalone FORTRAN library, *pi*; +a CMake package for that library; and a Python wrapper implemented in f2py. + +The example assume some familiarity with CMake and f2py, only really going into detail on the scikit-build parts. + +To install the package run in the project directory + +```bash +pip install . +``` + +To run the Python tests, first install the package, then in the project directory run + +```bash +pytest +``` + +This is slightly modified from the example in the [numpy docs](https://numpy.org/devdocs/f2py/buildtools/skbuild.html), but we are using Monte Carlo to estimate the value of $\pi$. + +A few surprises: +1. The dreaded underscore problem has a way of cropping up. One solution is explicitly writing out the interface in a [signature (`.pyf`) file](https://numpy.org/devdocs/f2py/signature-file.html). +2. The module will require numpy to work. +3. Between failed builds, it is best to clear out the `_skbuild` folder. diff --git a/projects/pi-fortran/pi/_pi.f b/projects/pi-fortran/pi/_pi.f new file mode 100644 index 0000000..be09dc9 --- /dev/null +++ b/projects/pi-fortran/pi/_pi.f @@ -0,0 +1,17 @@ + subroutine estimate_pi(pi_estimated, num_trials) +C Estimate pi using Monte Carlo + integer num_trials, num_in_circle + real*8 pi_estimated, x, y +Cf2py intent(in) num_trials +Cf2py intent(out) pi_estimated + + num_in_circle = 0d0 + do i = 1, num_trials + call random_number(x) + call random_number(y) + if (x**2 + y**2 < 1.0d0) then + num_in_circle = num_in_circle + 1 + endif + end do + pi_estimated = (4d0 * num_in_circle) / num_trials + end subroutine estimate_pi diff --git a/projects/pi-fortran/pi/pi.pyf b/projects/pi-fortran/pi/pi.pyf new file mode 100644 index 0000000..ef1fe39 --- /dev/null +++ b/projects/pi-fortran/pi/pi.pyf @@ -0,0 +1,10 @@ +! -*- f90 -*- +! Note: the context of this file is case sensitive. +pymodule pi +interface +subroutine estimate_pi(pi_estimated,num_trials) ! in pi/_pi.f + real*8 intent(out) :: pi_estimated + integer intent(in) :: num_trials +end subroutine estimate_pi +end interface +end pymodule pi diff --git a/projects/pi-fortran/pyproject.toml b/projects/pi-fortran/pyproject.toml new file mode 100644 index 0000000..cb3eef4 --- /dev/null +++ b/projects/pi-fortran/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = [ + "scikit-build-core", + "numpy>=1.21", +] +build-backend = "scikit_build_core.build" + +[project] +name = "pi-fortran" +version = "1.0.1" +requires-python = ">=3.7" +dependencies = ["numpy>=1.21"] + +[tool.scikit-build] +ninja.minimum-version = "1.10" +cmake.minimum-version = "3.17.2" diff --git a/projects/pi-fortran/tests/test_pi.py b/projects/pi-fortran/tests/test_pi.py new file mode 100644 index 0000000..e1c505a --- /dev/null +++ b/projects/pi-fortran/tests/test_pi.py @@ -0,0 +1,9 @@ +import math + +import pytest +from pi import pi + + +def test_estimate_pi(): + pi_est = pi.estimate_pi(1e8) + assert pi_est == pytest.approx(math.pi, rel=0.001)