diff --git a/.editorconfig b/.editorconfig index 8d3905d..e269037 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,7 @@ root = true [*] end_of_line = lf insert_final_newline = true +trim_trailing_whitespace = false # Matches multiple files with brace expansion notation # Set default charset diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24b728d..e768a00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,20 @@ concurrency: cancel-in-progress: true jobs: + typos: + name: Spelling (typos) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: crate-ci/typos@master + + ruff: + name: Linting (ruff) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 + test: name: Unittest (${{ matrix.os }}-${{ matrix.compiler }}-py${{ matrix.python-version }}) runs-on: ${{ matrix.os }} diff --git a/docs/conf.py b/docs/conf.py index b226c7d..306d707 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,38 +1,42 @@ -import sys import os +import sys from unittest.mock import MagicMock as Mock + from setuptools_scm import get_version + MOCK_MODULES = [ - 'numpy', 'numpy.testing', 'numpy.random', - 'symengine', 'symengine.printing', 'symengine.lib.symengine_wrapper', - 'jitcxde_common.helpers','jitcxde_common.numerical','jitcxde_common.symbolic' + "numpy", "numpy.testing", "numpy.random", + "symengine", "symengine.printing", "symengine.lib.symengine_wrapper", + "jitcxde_common.helpers","jitcxde_common.numerical","jitcxde_common.symbolic" ] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) -class GroupHandler_mock(object): pass -sys.modules['jitcxde_common.transversal'] = Mock(GroupHandler=GroupHandler_mock) +class GroupHandler_mock: + pass + +sys.modules["jitcxde_common.transversal"] = Mock(GroupHandler=GroupHandler_mock) sys.path.insert(0,os.path.abspath("../examples")) sys.path.insert(0,os.path.abspath("../jitcsde")) -needs_sphinx = '1.3' +needs_sphinx = "1.3" extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.mathjax', - 'numpydoc', + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.mathjax", + "numpydoc", ] -source_suffix = '.rst' +source_suffix = ".rst" -master_doc = 'index' +master_doc = "index" -project = u'JiTCSDE' -copyright = u'2017, Gerrit Ansmann' +project = "JiTCSDE" +copyright = "2017, Gerrit Ansmann" -release = version = get_version(root='..', relative_to=__file__) +release = version = get_version(root="..", relative_to=__file__) default_role = "any" @@ -40,18 +44,18 @@ class GroupHandler_mock(object): pass add_module_names = False -html_theme = 'nature' -pygments_style = 'colorful' -htmlhelp_basename = 'JiTCSDEdoc' +html_theme = "nature" +pygments_style = "colorful" +htmlhelp_basename = "JiTCSDEdoc" numpydoc_show_class_members = False -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" def on_missing_reference(app, env, node, contnode): - if node['reftype'] == 'any': + if node["reftype"] == "any": return contnode else: return None def setup(app): - app.connect('missing-reference', on_missing_reference) + app.connect("missing-reference", on_missing_reference) diff --git a/docs/index.rst b/docs/index.rst index 746fa97..773e471 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -104,7 +104,7 @@ References .. [RN17] C. Rackauckas, Q. Nie: Adaptive methods for stochastic differential equations via natural embeddings and rejection sampling with memory, Discrete Cont. Dyn.-B 22, pp. 2731–2761 (2017), `10.3934/dcdsb.2017133 `_. -.. [R10] A. Rößler, Runge–Kutta methods for the strong approximation of solutions of stochastic differential equations, SIAM J. Numer. Anal. 48, pp. 922–952 (2010) `10.1137/09076636X `_. +.. [R10] A. Rößler, Runge–Kutta methods for the strong approximation of solutions of stochastic differential equations, SIAM J. Numerical Anal. 48, pp. 922–952 (2010) `10.1137/09076636X `_. .. _JiTCODE: http://github.com/neurophysik/jitcode diff --git a/examples/noisy_and_jumpy_lorenz.py b/examples/noisy_and_jumpy_lorenz.py index b7e04d8..ead7be8 100644 --- a/examples/noisy_and_jumpy_lorenz.py +++ b/examples/noisy_and_jumpy_lorenz.py @@ -1,6 +1,3 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - """ As an example, suppose that we want to add jumps to the noisy Lorenz oscillator from `example`. These shall have exponentially distributed waiting times (i.e. they are a Poisson process) with a scale parameter :math:`β=1.0`. @@ -29,10 +26,12 @@ """ if __name__ == "__main__": - from jitcsde import y, jitcsde_jump - import numpy + import numpy as np import symengine + + from jitcsde import jitcsde_jump, y + rng = np.random.default_rng(seed=42) ρ = 28 σ = 10 β = symengine.Rational(8,3) @@ -47,22 +46,22 @@ g = [ p*y(i) for i in range(3) ] def IJI(time,state): - return numpy.random.exponential(1.0) + return rng.exponential(1.0) def jump(time,state): - return numpy.array([ + return np.array([ 0.0, 0.0, - numpy.random.normal(0.0,abs(state[2])) + rng.normal(0.0,abs(state[2])) ]) SDE = jitcsde_jump(IJI,jump,f,g) - initial_state = numpy.random.random(3) + initial_state = rng.random(3) SDE.set_initial_value(initial_state,0.0) data = [] - for time in numpy.arange(0.0, 100.0, 0.01): + for time in np.arange(0.0, 100.0, 0.01): data.append( SDE.integrate(time) ) - numpy.savetxt("timeseries.dat", data) + np.savetxt("timeseries.dat", data) diff --git a/examples/noisy_lorenz.py b/examples/noisy_lorenz.py index b14f3e2..0df53ad 100644 --- a/examples/noisy_lorenz.py +++ b/examples/noisy_lorenz.py @@ -1,7 +1,4 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - -""" +r""" Suppose we want to integrate Lorenz oscillator each of whose components is subject to a diffusion that amounts to :math:`p` of the respective component, i.e.: .. math:: @@ -72,10 +69,12 @@ """ if __name__ == "__main__": - from jitcsde import y, jitcsde - import numpy + import numpy as np import symengine + + from jitcsde import jitcsde, y + rng = np.random.default_rng(seed=42) ρ = 28 σ = 10 β = symengine.Rational(8,3) @@ -91,11 +90,11 @@ SDE = jitcsde(f,g) - initial_state = numpy.random.random(3) + initial_state = rng.random(3) SDE.set_initial_value(initial_state,0.0) data = [] - for time in numpy.arange(0.0, 100.0, 0.01): + for time in np.arange(0.0, 100.0, 0.01): data.append( SDE.integrate(time) ) - numpy.savetxt("timeseries.dat", data) + np.savetxt("timeseries.dat", data) diff --git a/jitcsde/__init__.py b/jitcsde/__init__.py index e74166e..713e6f9 100644 --- a/jitcsde/__init__.py +++ b/jitcsde/__init__.py @@ -1,12 +1,8 @@ -from ._jitcsde import ( - jitcsde, jitcsde_jump, - t, y, - UnsuccessfulIntegration, - test - ) +from ._jitcsde import UnsuccessfulIntegration, jitcsde, jitcsde_jump, t, test, y # noqa: F401 + try: - from .version import version as __version__ + from .version import version as __version__ # noqa: F401 except ImportError: from warnings import warn - warn('Failed to find (autogenerated) version.py. Do not worry about this unless you really need to know the version.') + warn("Failed to find (autogenerated) version.py. Do not worry about this unless you really need to know the version.", stacklevel=2) diff --git a/jitcsde/_jitcsde.py b/jitcsde/_jitcsde.py index 9346160..4fb9cf1 100644 --- a/jitcsde/_jitcsde.py +++ b/jitcsde/_jitcsde.py @@ -1,17 +1,17 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - -from warnings import warn -from itertools import count, chain -from os import path as path -import shutil import random -import symengine +import shutil +from itertools import chain, count +from os import path as path +from warnings import warn + import numpy as np -from jitcxde_common import jitcxde, checker -from jitcxde_common.helpers import sort_helpers, sympify_helpers, copy_helpers, filter_helpers, find_dependent_helpers +import symengine + +from jitcxde_common import checker, jitcxde +from jitcxde_common.helpers import copy_helpers, filter_helpers, find_dependent_helpers, sort_helpers, sympify_helpers from jitcxde_common.symbolic import collect_arguments, has_function + #: the symbol for the state that must be used to define the differential equation. It is a function and the integer argument denotes the component. You may just as well define an analogous function directly with SymEngine or SymPy, but using this function is the best way to get the most of future versions of JiTCSDE, in particular avoiding incompatibilities. You can import a SymPy variant from the submodule `sympy_symbols` instead (see `SymPy vs. SymEngine`_ for details). y = symengine.Function("y") @@ -62,7 +62,7 @@ class jitcsde(jitcxde): * A SymEngine function object used in `f_sym` to represent the function call. If you want to use any JiTCSDE features that need the derivative, this must have a properly defined `f_diff` method with the derivative being another callback function (or constant). * The Python function to be called. This function will receive the state array (`y`) as the first argument. All further arguments are whatever you use as arguments of the SymEngine function in `f_sym`. These can be any expression that you might use in the definition of the derivative and contain, e.g., dynamical variables, time, control parameters, and helpers. The only restriction is that the arguments are floats (and not vectors or similar). The return value must also be a float (or something castable to float). It is your responsibility to ensure that this function adheres to these criteria, is deterministic and sufficiently smooth with respect its arguments; expect nasty errors otherwise. - * The number of arguments, **excluding** the state array as mandatory first argument. This means if you have a variadic Python function, you cannot just call it with different numbers of arguments in `f_sym`, but you have to define separate callbacks for each of numer of arguments. + * The number of arguments, **excluding** the state array as mandatory first argument. This means if you have a variadic Python function, you cannot just call it with different numbers of arguments in `f_sym`, but you have to define separate callbacks for each of number of arguments. See `this example `_ (for JiTCDDE) for how to use this. @@ -86,7 +86,7 @@ def __init__( self, module_location = None ): - super(jitcsde,self).__init__(n,verbose,module_location) + super().__init__(n,verbose,module_location) if f_sym and not g_sym and not module_location: raise ValueError("You gave f_sym as an argument but neither g_sym nor module_location. JiTCSDE cannot properly work with this.") @@ -102,7 +102,7 @@ def __init__( self, self._determine_additivity(additive) if not ito: if self.additive: - warn("No need for conversion from Stratonovich to Itō for additive SDE.") + warn("No need for conversion from Stratonovich to Itō for additive SDE.", stacklevel=2) else: self._stratonovich_to_ito() @@ -117,7 +117,7 @@ def __init__( self, def _determine_additivity(self,additive): if additive is None: self.additive = ( - all( not has_function(entry ,y) for entry in self.g_sym() ) + all( not has_function(entry ,y) for entry in self.g_sym() ) and all( not has_function(helper[1],y) for helper in self._g_helpers ) ) else: @@ -211,7 +211,7 @@ def t(self, value): @checker def _check_non_empty(self): for function,name in [(self.f_sym, "f_sym"), (self.g_sym, "g_sym")]: - self._check_assert( function(), "%s is empty."%name ) + self._check_assert( function(), f"{name} is empty." ) @checker def _check_valid_arguments(self): @@ -220,11 +220,11 @@ def _check_valid_arguments(self): for argument in collect_arguments(entry,y): self._check_assert( argument[0] >= 0, - "y is called with a negative argument (%i) in component %i of %s." % (argument[0],i,name) + f"y is called with a negative argument ({argument[0]}) in component {i} of {name}." ) self._check_assert( argument[0] < self.n, - "y is called with an argument (%i) higher than the system’s dimension (%i) in component %i of %s." % (argument[0], self.n,i,name) + f"y is called with an argument ({argument[i]}) higher than the system’s dimension ({self.n}) in component {i} of {name}." ) @checker @@ -236,7 +236,7 @@ def _check_valid_symbols(self): for symbol in entry.atoms(symengine.Symbol): self._check_assert( symbol in valid_symbols, - "Invalid symbol (%s) in component %i of %s." % (symbol.name,i,name) + f"Invalid symbol ({symbol.name}) in component {i} of {name}." ) def reset_integrator(self): @@ -350,7 +350,7 @@ def compile_C( if simplify is None: simplify = self.n<=10 - helper_lengths = dict() + helper_lengths = {} for sym,helpers,name,long_name in [ ( self.f_sym, self._f_helpers, "f", "drift" ), @@ -535,10 +535,10 @@ def set_integration_parameters(self, if first_step > max_step: first_step = max_step - warn("Decreasing first_step to match max_step") + warn("Decreasing first_step to match max_step", stacklevel=2) if min_step > first_step: min_step = first_step - warn("Decreasing min_step to match first_step") + warn("Decreasing min_step to match first_step", stacklevel=2) assert decrease_threshold>=1.0, "decrease_threshold smaller than 1" assert increase_threshold<=1.0, "increase_threshold larger than 1" @@ -548,7 +548,7 @@ def set_integration_parameters(self, assert atol>=0.0, "negative atol" assert rtol>=0.0, "negative rtol" if atol==0 and rtol==0: - warn("atol and rtol are both 0. You probably do not want this.") + warn("atol and rtol are both 0. You probably do not want this.", stacklevel=2) self.atol = atol self.rtol = rtol @@ -568,15 +568,15 @@ def set_integration_parameters(self, def _control_for_min_step(self): if self.dt < self.min_step: - raise UnsuccessfulIntegration("\n" + raise UnsuccessfulIntegration( + "\n" "Could not integrate with the given tolerance parameters:\n\n" - "atol: %e\n" - "rtol: %e\n" - "min_step: %e\n\n" + f"atol: {self.atol:e}\n" + f"rtol: {self.rtol:e}\n" + f"min_step: {self.min_step:e}\n\n" "The most likely reasons for this are:\n" "• The SDE is ill-posed or stiff.\n" - "• You did not allow for an absolute error tolerance (atol) though your SDE calls for it. Even a very small absolute tolerance (1e-16) may sometimes help." - % (self.atol, self.rtol, self.min_step)) + "• You did not allow for an absolute error tolerance (atol) though your SDE calls for it. Even a very small absolute tolerance (1e-16) may sometimes help.") def _adjust_step_size(self, actual_dt): """ @@ -650,7 +650,7 @@ def pin_noise(self, number, step_size): assert number>=0, "Number must be non-negative" assert step_size>0, "Step size must be positive" if not isinstance(number,int): - warn("`number` does not appear to be a integer. This is very likely cause an error immediately.") + warn("`number` does not appear to be a integer. This is very likely cause an error immediately.", stacklevel=2) self.SDE.pin_noise(number,step_size) class jitcsde_jump(jitcsde): @@ -668,13 +668,16 @@ class jitcsde_jump(jitcsde): This must be a NumPy array, even if your system is one-dimensional. """ - def __init__( self, IJI, amp, *args, **kwargs ): - if not kwargs.pop("ito",True): + def __init__( self, IJI, amp, *args, rng=None, ito=True, **kwargs ): + if not ito: raise NotImplementedError("I don’t know how to convert jumpy Stratonovich SDEs to Itō SDEs – nobody does.") - super(jitcsde_jump,self).__init__(*args, **kwargs) + if rng is None: + rng = np.random.default_rng() + super().__init__(*args, **kwargs) self.IJI = IJI self.amp = amp self._next_jump = None + self.rng = rng @property def next_jump(self): @@ -684,29 +687,29 @@ def next_jump(self): return self._next_jump def reset_integrator(self): - super(jitcsde_jump,self).reset_integrator() + super().reset_integrator() self._next_jump = None def integrate(self, target_time): while self.next_jump