diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccd602eea..bd5d14309 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,25 +30,6 @@ jobs: pip install ruff ruff check - pylint: - name: Pylint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: "Main Script" - run: | - export EXTRA_INSTALL="pyvisfile matplotlib" - - curl -L -O https://tiker.net/ci-support-v0 - . ci-support-v0 - - # pylint seems unable to find the cython bits if not installed - # editable. -AK, 2023-11-01 - PROJECT_INSTALL_FLAGS="--editable" - - build_py_project_in_conda_env - run_pylint "$(get_proj_name)" examples/*.py test/*.py - mypy: name: Mypy runs-on: ubuntu-latest diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ea604e7f6..c6aabfb73 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -90,23 +90,6 @@ Documentation: tags: - python3 -Pylint: - script: | - export EXTRA_INSTALL="Cython pybind11 numpy scipy mako matplotlib" - curl -L -O https://tiker.net/ci-support-v0 - . ci-support-v0 - - # pylint seems unable to find the cython bits if not installed - # editable. -AK, 2023-11-01 - PROJECT_INSTALL_FLAGS="--editable" - - build_py_project - run_pylint "$(get_proj_name)" examples/*.py test/*.py - tags: - - python3 - except: - - tags - Mypy: script: | curl -L -O https://tiker.net/ci-support-v0 diff --git a/.pylintrc-local.yml b/.pylintrc-local.yml deleted file mode 100644 index 57d5fdaf2..000000000 --- a/.pylintrc-local.yml +++ /dev/null @@ -1,10 +0,0 @@ -- arg: py-version - val: '3.10' - -- arg: ignore - val: - - old_diffop_primitives.py - - generalized_debye.py - - waveguide.py # See issue #116 on gitlab -- arg: extension-pkg-whitelist - val: pytential.qbx.target_specific diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..7b093856a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,19 @@ +find_package( + Python + COMPONENTS Interpreter Development.Module + REQUIRED) +include(UseCython) + +cython_transpile(pytential/qbx/target_specific/impl.pyx LANGUAGE C OUTPUT_VARIABLE pytential_c) + +python_add_library(impl + MODULE + "${pytential_c}" + pytential/qbx/target_specific/helmholtz_utils.c + WITH_SOABI) + +target_compile_options(impl PRIVATE -Wall -Ofast -fopenmp) +target_link_options(impl PRIVATE -fopenmp) +target_include_directories(impl PRIVATE ${CMAKE_SOURCE_DIR}/pytential/qbx/target_specific) + +install(TARGETS impl DESTINATION pytential/qbx/target_specific) diff --git a/doc/conf.py b/doc/conf.py index 26bf17aeb..ca4de5926 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,4 +37,6 @@ # Sphinx started complaining about these in 8.2.1(-ish) # -AK, 2025-02-24 ["py:class", r"TypeAliasForwardRef"], + ["py:class", r"arraycontext.container._UserDefinedArrayContainer"], + ["py:class", r"arraycontext.container._UserDefinedArithArrayContainer"], ] diff --git a/pyproject.toml b/pyproject.toml index fccf0b391..b6cbc6df6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,6 @@ [build-system] -build-backend = "setuptools.build_meta" -requires = [ - "cython>=0.25", - "setuptools>=63", -] +requires = ["scikit-build-core", "cython", "cython-cmake"] +build-backend = "scikit_build_core.build" [project] name = "pytential" @@ -164,3 +161,30 @@ module = [ "sympy.*", ] ignore_missing_imports = true + +[tool.basedpyright] +reportImplicitStringConcatenation = "none" +reportUnnecessaryIsInstance = "none" +reportUnusedCallResult = "none" +reportExplicitAny = "none" +reportUnusedParameter = "hint" + +# This reports even cycles that are qualified by 'if TYPE_CHECKING'. Not what +# we care about at this moment. +# https://github.com/microsoft/pyright/issues/746 +reportImportCycles = "none" + +pythonVersion = "3.10" +pythonPlatform = "All" + +[[tool.basedpyright.executionEnvironments]] +root = "test" +reportUnknownArgumentType = "hint" +reportAttributeAccessIssue = "hint" +reportOperatorIssue = "hint" +reportIndexIssue = "hint" +reportCallIssue = "hint" +reportArgumentType = "hint" +reportPossiblyUnboundVariable = "hint" +reportGeneralTypeIssues = "hint" +reportOptionalSubscript = "hint" diff --git a/pytential/linalg/proxy.py b/pytential/linalg/proxy.py index 23555cccb..122b27d71 100644 --- a/pytential/linalg/proxy.py +++ b/pytential/linalg/proxy.py @@ -22,15 +22,16 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import Any +from typing import Any, cast import numpy as np import numpy.linalg as la -from arraycontext import PyOpenCLArrayContext, flatten +from arraycontext import Array, ArrayContainer, PyOpenCLArrayContext, flatten from meshmode.discretization import Discretization from meshmode.dof_array import DOFArray +from numpy.typing import NDArray from pytools import memoize_in from pytential import GeometryCollection, bind, sym from pytential.symbolic.dof_desc import DOFDescriptorLike @@ -151,7 +152,7 @@ class ProxyPointSource(PointPotentialSource): def __init__(self, lpot_source: QBXLayerPotentialSource, - proxies: np.ndarray) -> None: + proxies: Array) -> None: """ :arg lpot_source: the layer potential for which the proxy are constructed. :arg proxies: an array of shape ``(ambient_dim, nproxies)`` containing @@ -174,7 +175,7 @@ def get_expansion_for_qbx_direct_eval(self, base_kernel, target_kernels): class ProxyPointTarget(PointsTarget): def __init__(self, lpot_source: QBXLayerPotentialSource, - proxies: np.ndarray) -> None: + proxies: Array) -> None: """ :arg lpot_source: the layer potential for which the proxy are constructed. This argument is kept for symmetry with :class:`ProxyPointSource`. @@ -227,9 +228,9 @@ class ProxyClusterGeometryData: srcindex: IndexList pxyindex: IndexList - points: np.ndarray - centers: np.ndarray - radii: np.ndarray + points: NDArray[Any] + centers: NDArray[Any] + radii: NDArray[Any] _cluster_radii: np.ndarray | None = None @@ -260,13 +261,13 @@ def as_sources(self) -> ProxyPointSource: lpot_source = self.places.get_geometry(self.dofdesc.geometry) assert isinstance(lpot_source, QBXLayerPotentialSource) - return ProxyPointSource(lpot_source, self.points) + return ProxyPointSource(lpot_source, cast("Array", self.points)) def as_targets(self) -> ProxyPointTarget: lpot_source = self.places.get_geometry(self.dofdesc.geometry) assert isinstance(lpot_source, QBXLayerPotentialSource) - return ProxyPointTarget(lpot_source, self.points) + return ProxyPointTarget(lpot_source, cast("Array", self.points)) # }}} @@ -632,12 +633,12 @@ def __call__(self, source_dd = self.places.auto_source source_dd = sym.as_dofdesc(source_dd) - radii = bind(self.places, sym.expansion_radii( - self.ambient_dim, dofdesc=source_dd))(actx) - center_int = bind(self.places, sym.expansion_centers( - self.ambient_dim, -1, dofdesc=source_dd))(actx) - center_ext = bind(self.places, sym.expansion_centers( - self.ambient_dim, +1, dofdesc=source_dd))(actx) + radii = cast(ArrayContainer, bind(self.places, sym.expansion_radii( + self.ambient_dim, dofdesc=source_dd))(actx)) + center_int = cast(ArrayContainer, bind(self.places, sym.expansion_centers( + self.ambient_dim, -1, dofdesc=source_dd))(actx)) + center_ext = cast(ArrayContainer, bind(self.places, sym.expansion_centers( + self.ambient_dim, +1, dofdesc=source_dd))(actx)) return super().__call__(actx, source_dd, dof_index, expansion_radii=flatten(radii, actx), diff --git a/pytential/qbx/utils.py b/pytential/qbx/utils.py index 13b9701fc..5338cd1d6 100644 --- a/pytential/qbx/utils.py +++ b/pytential/qbx/utils.py @@ -22,6 +22,7 @@ THE SOFTWARE. """ +from typing import cast import numpy as np from pytools import memoize_method, memoize_in, log_process @@ -327,7 +328,8 @@ def _make_centers(discr): # Build tree with sources and centers. Split boxes # only because of sources. - refine_weights = actx.np.zeros(nparticles, np.int32) + import pyopencl.array as cl_array + refine_weights = cast("cl_array.Array", actx.np.zeros(nparticles, np.int32)) refine_weights[:nsources].fill(1) refine_weights.finish() @@ -363,7 +365,8 @@ def _make_centers(discr): del box_to_class # Compute element => source relation - qbx_element_to_source_starts = actx.np.zeros(nelements + 1, tree.particle_id_dtype) + qbx_element_to_source_starts = cast("cl_array.Array", + actx.np.zeros(nelements + 1, tree.particle_id_dtype)) el_offset = 0 node_nr_base = 0 for group in density_discr.groups: diff --git a/pytential/source.py b/pytential/source.py index 8752ba05d..fdd141cee 100644 --- a/pytential/source.py +++ b/pytential/source.py @@ -30,7 +30,7 @@ from pytools import T, memoize_in from sumpy.fmm import UnableToCollectTimingData from sumpy.kernel import Kernel -from sumpy.p2p import P2PBase +from sumpy.p2p import P2P, P2PBase from pytential import sym @@ -112,10 +112,10 @@ class _SumpyP2PMixin: def get_p2p(self, actx: PyOpenCLArrayContext, target_kernels: tuple[Kernel, ...], - source_kernels: tuple[Kernel, ...] | None = None) -> P2PBase: + source_kernels: tuple[Kernel, ...] | None = None) -> P2P: @memoize_in(actx, (_SumpyP2PMixin, "p2p")) def p2p(target_kernels: tuple[Kernel, ...], - source_kernels: tuple[Kernel, ...] | None) -> P2PBase: + source_kernels: tuple[Kernel, ...] | None) -> P2P: if any(knl.is_complex_valued for knl in target_kernels): value_dtype = self.complex_dtype # type: ignore[attr-defined] else: diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index 0c78b7820..6c4cfe19f 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -20,7 +20,7 @@ THE SOFTWARE. """ -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Sequence from dataclasses import field from warnings import warn from functools import partial @@ -400,17 +400,16 @@ def make_stringifier(self, originating_stringifier=None): Operand: TypeAlias = ( - ArithmeticExpression | np.ndarray[Any, np.dtype[Any]] | MultiVector) -QBXForcedLimit: TypeAlias = int | Literal["avg"] | None + ArithmeticExpression + | np.ndarray[Any, np.dtype[Any]] + | MultiVector[ArithmeticExpression]) + +QBXForcedLimit: TypeAlias = Literal[-2, -1, 1, 1, "avg"] | None # NOTE: this will likely live in pymbolic at some point, but for now we take it! ArithmeticExpressionT = TypeVar("ArithmeticExpressionT", bound=ArithmeticExpression) -class _NoArgSentinel: - pass - - class cse_scope(cse_scope_base): # noqa: N801 DISCRETIZATION = "pytential_discretization" @@ -457,7 +456,7 @@ class ErrorExpression(Expression): """The error message to raise when this expression is encountered.""" -def make_sym_mv(name: str, num_components: int) -> MultiVector[Expression]: +def make_sym_mv(name: str, num_components: int) -> MultiVector[ArithmeticExpression]: return MultiVector(make_sym_vector(name, num_components)) @@ -637,7 +636,7 @@ def make_op(operand_i): # different.. def __init__(self, ref_axes: tuple[tuple[int, int], ...], - operand: ArithmeticExpression, + operand: Operand | None, dofdesc: DOFDescriptorLike) -> None: if isinstance(ref_axes, int): warn(f"Passing an 'int' as 'ref_axes' to {type(self).__name__!r} " @@ -682,7 +681,11 @@ def num_reference_derivative( return NumReferenceDerivative(ref_axes, expr, as_dofdesc(dofdesc)) -def reference_jacobian(func, output_dim, dim, dofdesc=None): +def reference_jacobian( + func: Sequence[ArithmeticExpression], + output_dim: int, + dim: int, + dofdesc: DOFDescriptorLike = None): """Return a :class:`numpy.ndarray` representing the Jacobian of a vector function with respect to the reference coordinates. """ @@ -697,7 +700,10 @@ def reference_jacobian(func, output_dim, dim, dofdesc=None): return jac -def parametrization_derivative_matrix(ambient_dim, dim, dofdesc=None): +def parametrization_derivative_matrix( + ambient_dim: int, + dim: int, + dofdesc: DOFDescriptorLike = None): """Return a :class:`numpy.ndarray` representing the derivative of the reference-to-global parametrization. """ @@ -710,7 +716,10 @@ def parametrization_derivative_matrix(ambient_dim, dim, dofdesc=None): "pd_matrix", cse_scope.DISCRETIZATION) -def parametrization_derivative(ambient_dim, dim, dofdesc=None): +def parametrization_derivative( + ambient_dim: int, + dim: int, + dofdesc: DOFDescriptorLike = None): """Return a :class:`pymbolic.geometric_algebra.MultiVector` representing the derivative of the reference-to-global parametrization. """ @@ -718,10 +727,13 @@ def parametrization_derivative(ambient_dim, dim, dofdesc=None): par_grad = parametrization_derivative_matrix(ambient_dim, dim, dofdesc) from pytools import product - return product(MultiVector(vec) for vec in par_grad.T) + return product(MultiVector[ArithmeticExpression](vec) for vec in par_grad.T) -def pseudoscalar(ambient_dim, dim=None, dofdesc=None): +def pseudoscalar( + ambient_dim: int, + dim: int | None = None, + dofdesc: DOFDescriptorLike = None): """ Same as the outer product of all parametrization derivative columns. """ diff --git a/pytential/target.py b/pytential/target.py index c1b6727c1..e0f874abb 100644 --- a/pytential/target.py +++ b/pytential/target.py @@ -93,7 +93,9 @@ def __init__(self, nodes: Array, normals: Array | None = None) -> None: @property def ambient_dim(self) -> int: - return self._nodes.shape[0] + adim = self._nodes.shape[0] + assert isinstance(adim, int) + return adim @property def ndofs(self) -> int: diff --git a/setup.py b/setup.py deleted file mode 100644 index af3a26dc0..000000000 --- a/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python - -import sys - -from Cython.Build import cythonize -from setuptools import setup -from setuptools.extension import Extension - -if sys.platform.startswith("linux"): - openmp_flag = ["-fopenmp"] -else: - # OpenMP can't be relied upon on MacOS. - # https://stackoverflow.com/questions/43555410/enable-openmp-support-in-clang-in-mac-os-x-sierra-mojave - openmp_flag = [] - - -ext_modules = [ - Extension( - "pytential.qbx.target_specific.impl", - sources=[ - "pytential/qbx/target_specific/impl.pyx", - "pytential/qbx/target_specific/helmholtz_utils.c", - ], - depends=[ - "pytential/qbx/target_specific/impl.h", - "pytential/qbx/target_specific/helmholtz_utils.h", - ], - extra_compile_args=["-Wall", "-Ofast", *openmp_flag], - extra_link_args=openmp_flag, - ), -] - -setup(ext_modules=cythonize(ext_modules)) diff --git a/test/extra_int_eq_data.py b/test/extra_int_eq_data.py index 4cfb7eb12..20cb7fcbb 100644 --- a/test/extra_int_eq_data.py +++ b/test/extra_int_eq_data.py @@ -1,3 +1,4 @@ +# pyright: reportIncompatibleVariableOverride=none, reportAssignmentType=none __copyright__ = "Copyright (C) 2014 Andreas Kloeckner" __license__ = """ diff --git a/test/test_symbolic.py b/test/test_symbolic.py index 726b59e0d..ddba7746d 100644 --- a/test/test_symbolic.py +++ b/test/test_symbolic.py @@ -439,9 +439,9 @@ def test_derivative_binder_expr(): # {{{ test_mapper_kernel_transformation_remover def _make_operator(ambient_dim: int, op_name: str, k: float, *, side: int = +1): - from sumpy.kernel import LaplaceKernel, HelmholtzKernel + from sumpy.kernel import LaplaceKernel, HelmholtzKernel, Kernel if k == 0: - kernel = LaplaceKernel(ambient_dim) + kernel: Kernel = LaplaceKernel(ambient_dim) kernel_arguments = {} else: kernel = HelmholtzKernel(ambient_dim)