diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bdecf9a..33236be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,15 +18,15 @@ jobs: include: # version number must be string, otherwise 3.10 becomes 3.1 - os: windows-latest - python-version: "3.10" + python-version: "3.11" - os: macos-latest - python-version: "3.6" + python-version: "3.8" - os: ubuntu-latest - python-version: "pypy-3.7" + python-version: "pypy-3.8" fail-fast: false steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - run: python -m pip install --upgrade pip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57dd316..5ea88bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,11 +30,12 @@ repos: - id: file-contents-sorter - id: trailing-whitespace -# Python linter (Flake8) -- repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 +# Ruff linter, replacement for flake8, isort, pydocstyle +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.0.280' hooks: - - id: flake8 + - id: ruff + args: [--fix, --exit-non-zero-on-fix] # Python formatting - repo: https://github.com/psf/black @@ -49,10 +50,3 @@ repos: - id: mypy args: [src] pass_filenames: false - -# doc string checking -- repo: https://github.com/PyCQA/pydocstyle - rev: 6.3.0 - hooks: - - id: pydocstyle - files: src/jacobi/.*\.py diff --git a/pyproject.toml b/pyproject.toml index bf92d45..af6883b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,39 @@ [build-system] -requires = [ - "setuptools>=42", - "setuptools_scm[toml]>=3.4", -] +requires = ["setuptools>=42", "setuptools_scm[toml]>=3.4"] build-backend = "setuptools.build_meta" +[project] +name = "jacobi" +requires-python = ">=3.8" +description = "Compute numerical derivatives" +authors = [{ name = "Hans Dembinski" }, { email = "hans.dembinski@gmail.com" }] +dynamic = ["version"] +dependencies = ["numpy"] +readme = "README.rst" +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Topic :: Scientific/Engineering", +] +license = { file = "LICENSE" } + +[project.urls] +repository = "https://github.com/hdembinski/jacobi" +documentation = "https://hdembinski.github.io/jacobi/" + +[project.optional-dependencies] +test = ["pytest", "pytest-benchmark"] +doc = ["sphinx", "sphinx-rtd-theme", "ipykernel"] +plot = ["numdifftools", "matplotlib"] + +[tool.setuptools.packages.find] +where = ["src"] + [tool.setuptools_scm] write_to = "src/jacobi/_version.py" @@ -12,3 +41,26 @@ write_to = "src/jacobi/_version.py" ignore_missing_imports = true allow_redefinition = true no_implicit_optional = false + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "--doctest-modules --strict-config --strict-markers -q -ra --ff" +testpaths = ["src/jacobi", "tests"] +filterwarnings = [ + "error::numpy.VisibleDeprecationWarning", + "error::DeprecationWarning", +] + +[tool.ruff] +select = ["E", "F", "D"] +extend-ignore = ["D203", "D212"] + +[tool.ruff.pydocstyle] +convention = "numpy" + +[tool.ruff.per-file-ignores] +"test_*.py" = ["B", "D"] +"tests/bench.py" = ["D"] +".ci/*.py" = ["D"] +"bench/*.py" = ["D"] +"doc/*.py" = ["D"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 4770c99..0000000 --- a/setup.cfg +++ /dev/null @@ -1,54 +0,0 @@ -[metadata] -name = jacobi -version = attr: jacobi._version.version -author = Hans Dembinski -author_email = hans.dembinski@gmail.com -description = Compute numerical derivatives. -license = MIT -license_files = - LICENSE -long_description = file: README.rst -long_description_content_type = text/x-rst -url = https://github.com/hdembinski/jacobi -project_urls = - Bug Tracker = https://github.com/hdembinski/jacobi/issues -classifiers = - Development Status :: 4 - Beta - License :: OSI Approved :: MIT License - Programming Language :: Python :: 3 - Operating System :: OS Independent - Operating System :: POSIX :: Linux - Operating System :: MacOS - Operating System :: Microsoft :: Windows - Topic :: Scientific/Engineering - Intended Audience :: Developers - -[options] -package_dir = - = src -packages = jacobi -python_requires = >=3.6 -install_requires = numpy >= 1.10 - -[options.extras_require] -test = - pytest - pytest-benchmark - types-setuptools -doc = - sphinx - sphinx-rtd-theme - ipykernel -plot = - numdifftools - matplotlib - -[flake8] -max-line-length = 90 -ignore = E203 - -[pydocstyle] -convention = numpy - -[tool:pytest] -testpaths = test diff --git a/setup.py b/setup.py deleted file mode 100644 index afbfd1e..0000000 --- a/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -import site -import sys -import setuptools # type: ignore - -# workaround to allow editable install as user -site.ENABLE_USER_SITE = "--user" in sys.argv[1:] - -setuptools.setup() diff --git a/src/jacobi/_jacobi.py b/src/jacobi/_jacobi.py index 16e26e4..09972f0 100644 --- a/src/jacobi/_jacobi.py +++ b/src/jacobi/_jacobi.py @@ -42,8 +42,8 @@ def jacobi( Whether to compute central (0), forward (1) or backward derivatives (-1). The default (None) uses auto-detection. mask : array or None, optional - If `x` is an array and `mask` is not None, compute the Jacobi matrix only for the - part of the array selected by the mask. + If `x` is an array and `mask` is not None, compute the Jacobi matrix only for + the part of the array selected by the mask. rtol : float, optional Relative tolerance for the derivative. The algorithm stops when this relative tolerance is reached. If 0 (the default), the algorithm iterates until the diff --git a/src/jacobi/_propagate.py b/src/jacobi/_propagate.py index fa8f5fd..1092a55 100644 --- a/src/jacobi/_propagate.py +++ b/src/jacobi/_propagate.py @@ -24,11 +24,12 @@ def propagate( Parameters ---------- fn: callable - Function with the signature `fn(x, *args)`, where `x` is a number or a sequence - of numbers and `*args` are optional auxiliary arguments. The function must - return a number or a sequence of numbers (ideally as a numpy array). The - length of `x` can differ from the output sequence. Error propagation is only - performed with respect to `x`, the auxiliary arguments are ignored. + Function with the signature `fn(x, [y, ...])`, where `x` is a number or a + sequence of numbers, likewise if other arguments are present they must have the + same format. The function must return a number or a sequence of numbers (ideally + as a numpy array). The length of `x` can differ from the output sequence. The + function should accept more than one argument only if there are no + correlations between these arguments. See example below for use cases. x: float or array-like with shape (N,) Input vector. An array-like is converted before passing it to the callable. cov: float or array-like with shape (N,) or shape(N, N) @@ -53,63 +54,50 @@ def propagate( ----- For callables `fn` which perform only element-wise computation, the jacobian is a diagonal matrix. This special case is detected and the computation optimised, - although can further speed up the computation by passing the argumet `diagonal=True`. + although can further speed up the computation by passing the argument + `diagonal=True`. In this special case, error propagation works correctly even if the output of `fn` is NaN for some inputs. Examples -------- - General error propagation maps input vectors to output vectors:: - - def fn(x): - return x ** 2 + 1 - - x = [1, 2] - xcov = [[3, 1], - [1, 4]] - - y, ycov = propagate(fn, x, xcov) - - In the previous example, the function y = fn(x) treats all x values independently, - so the Jacobian computed from fn(x) has zero off-diagonal entries. In this case, - one can speed up the calculation significantly with a special keyword:: - - # same result as before, but faster and uses much less memory - y, ycov = propagate(fn, x, xcov, diagonal=True) - - If the function accepts several arguments, their uncertainties are treated as - uncorrelated:: - - def fn(x, y): - return x + y - - x = 1 - y = 2 - xcov = 2 - ycov = 3 - - z, zcov = propagate(fn, x, xcov, y, ycov) - - Functions that accept several correlated arguments must be wrapped:: - - def fn(x, y): - return x + y - - x = 1 - y = 2 - sigma_x = 3 - sigma_y = 4 - rho_xy = 0.5 - - r = [x, y] - cov_xy = rho_xy * sigma_x * sigma_y - rcov = [[sigma_x ** 2, cov_xy], [cov_xy, sigma_y ** 2]] - - def fn_wrapped(r): - return fn(r[0], r[1]) - - z, zcov = propagate(fn_wrapped, r, rcov) + General error propagation maps input vectors to output vectors. + + >>> def fn(x): + ... return x ** 2 + 1 + >>> x = [1, 2] + >>> xcov = [[3, 1], + ... [1, 4]] + >>> y, ycov = propagate(fn, x, xcov) + + In the previous example, the function ``y = fn(x)`` treats all x values + independently and the Jacobian computed from ``fn(x)`` has zero off-diagonal + entries. In this case, one can speed up the calculation significantly with a special + keyword. + + >>> y, ycov = propagate(fn, x, xcov, diagonal=True) + + This produces the same result, but is faster and uses less memory. If the function + accepts several arguments, their uncertainties are treated as uncorrelated. + + >>> def fn(x, y): + ... return x + y + >>> x = 1 + >>> y = 2 + >>> xcov = 2 + >>> ycov = 3 + >>> z, zcov = propagate(fn, x, xcov, y, ycov) + + Functions that accept several correlated arguments must be wrapped. + + >>> def fn(x, y): + ... return x + y + >>> rho_xy = 0.5 + >>> cov_xy = rho_xy * (xcov * ycov) ** 0.5 + >>> r = [x, y] + >>> rcov = [[xcov, cov_xy], [cov_xy, ycov]] + >>> z, zcov = propagate(lambda r: fn(r[0], r[1]), r, rcov) See Also -------- diff --git a/test/bench.py b/tests/bench.py similarity index 100% rename from test/bench.py rename to tests/bench.py diff --git a/test/test_jacobi.py b/tests/test_jacobi.py similarity index 100% rename from test/test_jacobi.py rename to tests/test_jacobi.py diff --git a/test/test_propagate.py b/tests/test_propagate.py similarity index 100% rename from test/test_propagate.py rename to tests/test_propagate.py