diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index dfc639a..c6e20a4 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -130,6 +130,15 @@ jobs: . venv-petsctools/bin/activate pytest petsctools-repo + - name: Run Cython demo + if: success() || steps.install-petsc4py.conclusion == 'success' + run: | + . venv-petsctools/bin/activate + cd petsctools-repo/docs/source/_static/cython_demo + CC=mpicc python setup.py build_ext --inplace + python -c "import slow" + python -c "import fast" + - name: Build documentation id: build_docs if: success() || steps.install.conclusion == 'success' diff --git a/.gitignore b/.gitignore index 614fae7..3a9e41a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,14 @@ petsctools/config.ini -*/__pycache__ +**/__pycache__ *.egg-info +build/ + docs/build docs/source/generated + +docs/source/_static/cython_demo/build +docs/source/_static/cython_demo/*.c +docs/source/_static/cython_demo/*.so +docs/source/_static/cython_demo/*.html diff --git a/docs/source/_static/cython_demo/fast.pyx b/docs/source/_static/cython_demo/fast.pyx new file mode 100644 index 0000000..fea9f2a --- /dev/null +++ b/docs/source/_static/cython_demo/fast.pyx @@ -0,0 +1,36 @@ +import time + +import cython +from petsc4py import PETSc + +from petsctools cimport cpetsc + + +def medium(): + N: cython.int = int(1e8) + section: PETSc.Section = PETSc.Section().create() + section.setChart(0, N) + + start = time.time() + i: cython.int + for i in range(N): + if i % 2 == 0: + section.setDof(i, 1) + print(f"Time elapsed: {time.time() - start}") + + +def fast(): + N: cython.int = int(1e8) + section: cpetsc.PetscSection_py = PETSc.Section().create() + section.setChart(0, N) + + start = time.time() + i: cython.int + for i in range(N): + if i % 2 == 0: + cpetsc.CHKERR(cpetsc.PetscSectionSetDof(section.sec, i, 1)) + print(f"Time elapsed: {time.time() - start}") + + +medium() +fast() diff --git a/docs/source/_static/cython_demo/setup.py b/docs/source/_static/cython_demo/setup.py new file mode 100644 index 0000000..ea2a8be --- /dev/null +++ b/docs/source/_static/cython_demo/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, Extension + +import petsc4py +import petsctools + + +extension = Extension( + name="fast", + language="c", + sources=["fast.pyx"], + include_dirs=[ + petsc4py.get_include(), + *petsctools.get_petsc_dirs(subdir="include"), + ], + library_dirs=petsctools.get_petsc_dirs(subdir="lib"), + runtime_library_dirs=petsctools.get_petsc_dirs(subdir="lib"), + libraries=["petsc", "mpi"], +) + +setup(ext_modules=[extension]) diff --git a/docs/source/_static/cython_demo/slow.py b/docs/source/_static/cython_demo/slow.py new file mode 100644 index 0000000..82acb94 --- /dev/null +++ b/docs/source/_static/cython_demo/slow.py @@ -0,0 +1,18 @@ +import time + +from petsc4py import PETSc + + +def slow(): + N = int(1e8) + section = PETSc.Section().create() + section.setChart(0, N) + + start = time.time() + for i in range(N): + if i % 2 == 0: + section.setDof(i, 1) + print(f"Time elapsed: {time.time() - start}") + + +slow() diff --git a/docs/source/cython.rst b/docs/source/cython.rst new file mode 100644 index 0000000..63d8b0a --- /dev/null +++ b/docs/source/cython.rst @@ -0,0 +1,99 @@ +PETSc C bindings +---------------- + +In some circumstances it is desirable to write PETSc C code instead of using +the petsc4py Python bindings. For example: + +* The overhead involved in calling Python bindings may be unacceptable. This + may be the case in very tight loops. +* The desired API functionality is not available in petsc4py. + +To support this, petsctools makes some of PETSc's C API available to use +through `Cython `_. It does this in a similar way +to petsc4py but emphasises readability and faithfulness to the API. + +Demo +~~~~ + +To demonstrate this, consider the following simple piece of PETSc code: + +.. literalinclude:: _static/cython_demo/slow.py + :language: python3 + +This code is written in Python and consists of petsc4py API calls. The +Python overhead is therefore maximised. Run on the author's machine this +code takes 4.0s to run to completion. + +Compare this to the examples given in this Cython file: + +.. literalinclude:: _static/cython_demo/fast.pyx + :language: cython + +Here we have two cases. The first (``medium``) is very similar to +``slow`` but is able to partially compile itself because it is +written in Cython. The second (``fast``) avoids petsc4py entirely +and instead uses the PETSc C API directly as exposed through +petsctools. + +Switching to Cython and using the C API directly both lead to +performance improvements. ``medium`` takes 1.8s and ``fast`` +takes 0.78s. + +Compiling Cython code +~~~~~~~~~~~~~~~~~~~~~ + +To compile the Cython code it must be registered as a compiled +extension inside a ``setup.py`` file. A working example for the +extension provided above looks like: + +.. literalinclude:: _static/cython_demo/setup.py + :language: python3 + +This file should be added to your project along with a suitable +``pyproject.toml`` that indicates ``setuptools`` as the +``build-backend``. ``setuptools``, ``cython``, ``petsc4py`` and +``petsctools`` are all necessary build-time dependencies. + +.. note:: + If you encounter errors like:: + + /.../petsc/include/petscsys.h:124:12: fatal error: mpi.h: No such file or directory + 124 | #include + | ^~~~~~~ + compilation terminated. + error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1 + + then this usually means that setuptools cannot find your MPI distribution. + To fix this simply set the environment variable ``CC=mpicc`` and + try again. + +Conventions used +~~~~~~~~~~~~~~~~ + +* All objects are available inside the ``cpetsc`` namespace after + ``cimport``-ing it (e.g. ``cpetsc.PetscSectionSetDof``). + +* petsc4py PETSc objects are renamed to their C API equivalent with + a ``_py`` suffix. For example ``cpetsc.PetscSection`` represents the + C type and ``cpetsc.PetscSection_py`` the Cython type. + +* The C handle of the petsc4py objects are available through a specific + attribute that depends on the type. Examples include: + + * ``cpetsc.Mat_py.mat`` ⟷ ``cpetsc.Mat`` + * ``cpetsc.Vec_py.vec`` ⟷ ``cpetsc.Vec`` + * ``cpetsc.IS_py.iset`` ⟷ ``cpetsc.IS`` + * ``cpetsc.PetscSection_py.sec`` ⟷ ``cpetsc.PetscSection`` + + For more information you will have to refer to the `petsc4py + source code `__. + +Adding more functions +~~~~~~~~~~~~~~~~~~~~~ + +Adding additional PETSc functions to petsctools is straightforward. You simply +have to add the bindings to the +`definitions file `__ +in a declarative way, just as you would write any other C file. For more +information please refer to the `Cython documentation +`__. diff --git a/docs/source/index.rst b/docs/source/index.rst index 5a7cdd9..978da07 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,4 +8,5 @@ petsctools provides Pythonic extensions for petsc4py and slepc4py. :maxdepth: 2 examples + cython generated/modules diff --git a/petsctools/__init__.pxd b/petsctools/__init__.pxd new file mode 100644 index 0000000..e69de29 diff --git a/petsctools/cpetsc.pxd b/petsctools/cpetsc.pxd new file mode 100644 index 0000000..d04efb1 --- /dev/null +++ b/petsctools/cpetsc.pxd @@ -0,0 +1,60 @@ +"""This file exposes the PETSc API as a module for use in Cython.""" + +# IMPORTANT: This file cannot be accessed if petsctools is installed in editable mode. + +from petsc4py cimport PETSc as _PETSc + +# clearer aliases from petsc4py, so the names here match the C API +ctypedef _PETSc.PetscMat Mat +ctypedef _PETSc.Mat Mat_py +ctypedef _PETSc.PetscSF PetscSF +ctypedef _PETSc.SF PetscSF_py +ctypedef _PETSc.PetscSection PetscSection +ctypedef _PETSc.Section PetscSection_py +ctypedef _PETSc.PetscIS IS +ctypedef _PETSc.IS IS_py + +# other PETSc imports +from petsc4py.PETSc cimport ( + CHKERR, + PetscErrorCode, +) + + +cdef extern from "petsc.h": + # fundamental types + ctypedef long PetscInt + ctypedef double PetscReal + ctypedef double PetscScalar + ctypedef enum PetscBool: + PETSC_TRUE + PETSC_FALSE + ctypedef enum InsertMode: + INSERT_VALUES + ADD_VALUES + ctypedef enum PetscCopyMode: + PETSC_COPY_VALUES + PETSC_OWN_POINTER + PETSC_USE_POINTER + + # memory management + PetscErrorCode PetscCalloc1(size_t,void*) + PetscErrorCode PetscMalloc1(size_t,void*) + PetscErrorCode PetscFree(void*) + + # Mat + PetscErrorCode MatSetValue(Mat,PetscInt,PetscInt,const PetscScalar,InsertMode) + PetscErrorCode MatSetValuesBlockedLocal(Mat,PetscInt,const PetscInt[],PetscInt,const PetscInt[],const PetscScalar[],InsertMode) + + # PetscSF + ctypedef struct PetscSFNode: + PetscInt rank + PetscInt index + + PetscErrorCode PetscSFGetGraph(PetscSF,PetscInt*,PetscInt*,const PetscInt**,const PetscSFNode**) + PetscErrorCode PetscSFSetGraph(PetscSF,PetscInt,PetscInt,PetscInt*,PetscCopyMode,PetscSFNode*,PetscCopyMode) + + # PetscSection + PetscErrorCode PetscSectionGetDof(PetscSection,PetscInt,PetscInt*) + PetscErrorCode PetscSectionSetDof(PetscSection,PetscInt,PetscInt) + PetscErrorCode PetscSectionGetOffset(PetscSection,PetscInt,PetscInt*) diff --git a/pyproject.toml b/pyproject.toml index 0d155d8..0f9015d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,13 @@ ci = [ {include-group = "docs"}, {include-group = "lint"}, {include-group = "test"}, + "cython", + "setuptools", ] +[tool.setuptools.package-data] +petsctools = ["__init__.pxd", "cpetsc.pxd"] + [tool.ruff] line-length = 79