From 331ecaea1a8c2c6240a4edf4077d0069fdb139d0 Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Mon, 8 Aug 2022 17:14:06 +0200 Subject: [PATCH 01/13] style: fix indent `.flake8` config --- .flake8 | 108 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/.flake8 b/.flake8 index 98cb81852..1701680f1 100644 --- a/.flake8 +++ b/.flake8 @@ -1,65 +1,65 @@ [flake8] application-import-names = - ampform + ampform filename = - ./docs/*.py - ./src/*.py - ./tests/*.py + ./docs/*.py + ./src/*.py + ./tests/*.py exclude = - **/__pycache__ - **/_build - *.pyi - /typings/** + **/__pycache__ + **/_build + *.pyi + /typings/** ignore = - # False positive with attribute docstrings - B018 - # https://github.com/psf/black#slices - E203 - # allowed by black - E231 - # https://github.com/psf/black#line-length - E501 - # should be possible to use {} in latex strings - FS003 - # block quote ends without a blank line (black formatting) - RST201 - # missing pygments - RST299 - # unexpected indentation (related to google style docstring) - RST301 - # false-positive error in math directive - RST307 - # enforce type ignore with mypy error codes (combined --extend-select=TI100) - TI1 - # https://github.com/psf/black#line-breaks--binary-operators - W503 + # False positive with attribute docstrings + B018 + # https://github.com/psf/black#slices + E203 + # allowed by black + E231 + # https://github.com/psf/black#line-length + E501 + # should be possible to use {} in latex strings + FS003 + # block quote ends without a blank line (black formatting) + RST201 + # missing pygments + RST299 + # unexpected indentation (related to google style docstring) + RST301 + # false-positive error in math directive + RST307 + # enforce type ignore with mypy error codes (combined --extend-select=TI100) + TI1 + # https://github.com/psf/black#line-breaks--binary-operators + W503 extend-select = - TI100 + TI100 per-file-ignores = - # unused imports for backward compatibility - src/ampform/dynamics/__init__.py:F401 - # λ symbols for DPD paper - src/ampform/helicity/align/dpd.py:N806 + # unused imports for backward compatibility + src/ampform/dynamics/__init__.py:F401 + # λ symbols for DPD paper + src/ampform/helicity/align/dpd.py:N806 radon-max-cc = 8 radon-no-assert = True rst-roles = - attr - cite - class - doc - download - eq - file - func - meth - mod - pdg-review - ref - term + attr + cite + class + doc + download + eq + file + func + meth + mod + pdg-review + ref + term rst-directives = - autolink-preface - automethod - deprecated - envvar - exception - seealso + autolink-preface + automethod + deprecated + envvar + exception + seealso From e33edd2fb77e93f69789bdd2ca1c40be5a90d884 Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Mon, 8 Aug 2022 17:14:07 +0200 Subject: [PATCH 02/13] style: sort options in `tox.ini` alphabetically --- tox.ini | 55 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/tox.ini b/tox.ini index 330bed006..0f9014cf0 100644 --- a/tox.ini +++ b/tox.ini @@ -33,11 +33,6 @@ description = Build documentation and API through Sphinx allowlist_externals = sphinx-build -passenv = - EXECUTE_NB - GITHUB_REPO - GITHUB_TOKEN - READTHEDOCS_VERSION commands = sphinx-build \ --color \ @@ -45,18 +40,17 @@ commands = -TW \ -b html \ docs/ docs/_build/html +passenv = + EXECUTE_NB + GITHUB_REPO + GITHUB_TOKEN + READTHEDOCS_VERSION [testenv:doclive] description = Set up a server to directly preview changes to the HTML pages allowlist_externals = sphinx-autobuild -passenv = - EXECUTE_NB - GITHUB_REPO - GITHUB_TOKEN - READTHEDOCS_VERSION - TERM commands = sphinx-autobuild \ --open-browser \ @@ -80,16 +74,16 @@ commands = --watch docs \ --watch src \ docs/ docs/_build/html - -[testenv:docnb] -description = - Build documentation through Sphinx WITH output of Jupyter notebooks passenv = + EXECUTE_NB GITHUB_REPO GITHUB_TOKEN READTHEDOCS_VERSION -setenv = - EXECUTE_NB = "yes" + TERM + +[testenv:docnb] +description = + Build documentation through Sphinx WITH output of Jupyter notebooks allowlist_externals = sphinx-build commands = @@ -99,6 +93,12 @@ commands = -TW \ -b html \ docs/ docs/_build/html +passenv = + GITHUB_REPO + GITHUB_TOKEN + READTHEDOCS_VERSION +setenv = + EXECUTE_NB = yes [testenv:docnb-force] description = @@ -126,9 +126,6 @@ commands = [testenv:linkcheck] description = Check external links in the documentation (requires internet connection) -passenv = - EXECUTE_NB - READTHEDOCS_VERSION allowlist_externals = sphinx-build commands = @@ -137,22 +134,23 @@ commands = -T \ -b linkcheck \ docs/ docs/_build/linkcheck +passenv = + EXECUTE_NB + READTHEDOCS_VERSION [testenv:nb] description = Run all notebooks with pytest -passenv = - EXECUTE_NB allowlist_externals = pytest commands = pytest --nbmake {posargs:docs} +passenv = + EXECUTE_NB [testenv:pydeps] description = Visualize module dependencies -deps = - pydeps changedir = src commands = pydeps ampform \ @@ -160,14 +158,17 @@ commands = --exclude *._* \ --max-bacon=1 \ --noshow -passenv = HOME +deps = + pydeps +passenv = + HOME [testenv:sty] description = Perform all linting, formatting, and spelling checks -setenv = - SKIP = pyright allowlist_externals = pre-commit commands = pre-commit run {posargs} -a +setenv = + SKIP = pyright From 5a6157fb573981818048f97b1d96be64ca0d47ab Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Tue, 18 Oct 2022 17:01:01 +0200 Subject: [PATCH 03/13] DX: activate `pyright` strict checking mode --- .vscode/settings.json | 1 + docs/_extend_docstrings.py | 2 +- docs/conf.py | 3 +++ pyrightconfig.json | 16 +++++++++++++++- tests/dynamics/test_builder.py | 1 + 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f68dc52f1..900318490 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -48,6 +48,7 @@ ], "python.analysis.autoImportCompletions": false, "python.analysis.diagnosticMode": "workspace", + "python.analysis.typeCheckingMode": "strict", "python.formatting.provider": "black", "python.linting.banditEnabled": false, "python.linting.enabled": true, diff --git a/docs/_extend_docstrings.py b/docs/_extend_docstrings.py index 147e3f44f..dcea2ece3 100644 --- a/docs/_extend_docstrings.py +++ b/docs/_extend_docstrings.py @@ -721,7 +721,7 @@ def _graphviz_to_image( # pylint: disable=too-many-arguments options = {} global _GRAPHVIZ_COUNTER # pylint: disable=global-statement output_file = f"graphviz_{_GRAPHVIZ_COUNTER}" - _GRAPHVIZ_COUNTER += 1 + _GRAPHVIZ_COUNTER += 1 # pyright: reportConstantRedefinition=false graphviz.Source(dot).render(f"{_IMAGE_DIR}/{output_file}", format=format) restructuredtext = "\n" if label: diff --git a/docs/conf.py b/docs/conf.py index 008dd15a5..3999b0ebc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,10 @@ import requests +# pyright: reportConstantRedefinition=false # pyright: reportMissingImports=false +# pyright: reportUntypedBaseClass=false +# pyright: reportUntypedFunctionDecorator=false from pybtex.database import Entry from pybtex.plugin import register_plugin from pybtex.richtext import Tag, Text diff --git a/pyrightconfig.json b/pyrightconfig.json index 2c33ce945..db4261754 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -2,10 +2,24 @@ "exclude": [".git", ".tox", "docs/_build", "docs/adr"], "include": ["docs", "src", "tests"], "reportGeneralTypeIssues": false, + "reportIncompatibleMethodOverride": false, + "reportMissingParameterType": false, + "reportMissingTypeArgument": false, + "reportMissingTypeStubs": false, + "reportOverlappingOverload": false, "reportPrivateImportUsage": false, + "reportPrivateUsage": false, "reportUnboundVariable": false, + "reportUnknownArgumentType": false, + "reportUnknownMemberType": false, + "reportUnknownParameterType": false, + "reportUnknownVariableType": false, + "reportUnnecessaryComparison": false, + "reportUnnecessaryContains": false, + "reportUnnecessaryIsInstance": false, "reportUnusedClass": true, "reportUnusedFunction": true, "reportUnusedImport": true, - "reportUnusedVariable": true + "reportUnusedVariable": true, + "typeCheckingMode": "strict" } diff --git a/tests/dynamics/test_builder.py b/tests/dynamics/test_builder.py index 370cafbac..d8661c2e1 100644 --- a/tests/dynamics/test_builder.py +++ b/tests/dynamics/test_builder.py @@ -89,6 +89,7 @@ def test_breit_wigner_with_energy_dependent_width( builder.form_factor = True bw_with_ff, parameters = builder(particle, variable_set) + # pyright: reportConstantRedefinition=false L = variable_set.angular_momentum # noqa: N806 form_factor = formulate_form_factor( s, m1, m2, angular_momentum=L, meson_radius=d From 470963519959f44fc23a5f0779c99f9f8dac0527 Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Mon, 8 Aug 2022 17:14:07 +0200 Subject: [PATCH 04/13] feat: implement `perform_cached_doit()` --- .flake8 | 1 + environment.yml | 2 + src/ampform/helicity/__init__.py | 2 +- src/ampform/sympy/__init__.py | 79 ++++++++++++++++++++++++++++++++ tests/sympy/test_caching.py | 51 +++++++++++++++++++++ tox.ini | 13 ++++++ 6 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 tests/sympy/test_caching.py diff --git a/.flake8 b/.flake8 index 1701680f1..c615fbf40 100644 --- a/.flake8 +++ b/.flake8 @@ -40,6 +40,7 @@ per-file-ignores = src/ampform/dynamics/__init__.py:F401 # λ symbols for DPD paper src/ampform/helicity/align/dpd.py:N806 + tests/sympy/test_caching.py:C408 radon-max-cc = 8 radon-no-assert = True rst-roles = diff --git a/environment.yml b/environment.yml index 1d3bb669d..6ec6838f6 100644 --- a/environment.yml +++ b/environment.yml @@ -9,3 +9,5 @@ dependencies: - | -c .constraints/py3.8.txt -e .[dev] +variables: + PYTHONHASHSEED: 0 diff --git a/src/ampform/helicity/__init__.py b/src/ampform/helicity/__init__.py index 6d9d29ef8..dd421af04 100644 --- a/src/ampform/helicity/__init__.py +++ b/src/ampform/helicity/__init__.py @@ -165,7 +165,7 @@ def unfold_poolsums(expr: sp.Expr) -> sp.Expr: intensity = self.intensity.evaluate() intensity = unfold_poolsums(intensity) - return intensity.subs(self.amplitudes) + return intensity.xreplace(self.amplitudes) def rename_symbols( # noqa: R701 self, renames: Iterable[tuple[str, str]] | Mapping[str, str] diff --git a/src/ampform/sympy/__init__.py b/src/ampform/sympy/__init__.py index ab56bf03f..bf539d2fb 100644 --- a/src/ampform/sympy/__init__.py +++ b/src/ampform/sympy/__init__.py @@ -5,9 +5,15 @@ from __future__ import annotations import functools +import hashlib import itertools +import logging +import os +import pickle import re from abc import abstractmethod +from os.path import abspath, dirname, expanduser +from textwrap import dedent from typing import Callable, Iterable, Sequence, SupportsFloat, TypeVar import sympy as sp @@ -16,6 +22,8 @@ from sympy.printing.numpy import NumPyPrinter from sympy.printing.precedence import PRECEDENCE +_LOGGER = logging.getLogger(__name__) + class UnevaluatedExpression(sp.Expr): """Base class for expression classes with an :meth:`evaluate` method. @@ -490,3 +498,74 @@ def determine_indices(symbol: sp.Basic) -> list[int]: except SyntaxError: return [] return list(indices) + + +def perform_cached_doit( + unevaluated_expr: sp.Expr, directory: str | None = None +) -> sp.Expr: + """Perform :meth:`~sympy.core.basic.Basic.doit` cache the result to disk. + + The cached result is fetched from disk if the hash of the original expression is the + same as the hash embedded in the filename. + + Args: + unevaluated_expr: A `sympy.Expr ` on which to call + :meth:`~sympy.core.basic.Basic.doit`. + directory: The directory in which to cache the result. If `None`, the cache + directory will be put under the home directory. + + .. tip:: For a faster cache, set `PYTHONHASHSEED + `_ to a + fixed value. + """ + if directory is None: + home_directory = expanduser("~") + directory = abspath(f"{home_directory}/.sympy-cache") + h = get_readable_hash(unevaluated_expr) + filename = f"{directory}/{h}.pkl" + os.makedirs(dirname(filename), exist_ok=True) + if os.path.exists(filename): + with open(filename, "rb") as f: + return pickle.load(f) + _LOGGER.warning( + f"Cached expression file {filename} not found, performing doit()..." + ) + unfolded_expr = unevaluated_expr.doit() + with open(filename, "wb") as f: + pickle.dump(unfolded_expr, f) + return unfolded_expr + + +def get_readable_hash(obj) -> str: + python_hash_seed = _get_python_hash_seed() + if python_hash_seed is not None: + return f"pythonhashseed-{python_hash_seed}{hash(obj):+}" + b = _to_bytes(obj) + return hashlib.sha256(b).hexdigest() + + +def _to_bytes(obj) -> bytes: + if isinstance(obj, sp.Expr): + # Using the str printer is slower and not necessarily unique, + # but pickle.dumps() does not always result in the same bytes stream. + _warn_about_unsafe_hash() + return str(obj).encode() + return pickle.dumps(obj) + + +def _get_python_hash_seed() -> int | None: + python_hash_seed = os.environ.get("PYTHONHASHSEED", "") + if python_hash_seed is not None and python_hash_seed.isdigit(): + return int(python_hash_seed) + return None + + +@functools.lru_cache(maxsize=None) # warn once +def _warn_about_unsafe_hash(): + message = """ + PYTHONHASHSEED has not been set. For faster and safer hashing of SymPy expressions, + set the PYTHONHASHSEED environment variable to a fixed value and rerun the program. + See https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHASHSEED + """ + message = dedent(message).replace("\n", " ").strip() + _LOGGER.warning(message) diff --git a/tests/sympy/test_caching.py b/tests/sympy/test_caching.py new file mode 100644 index 000000000..1729a0d1c --- /dev/null +++ b/tests/sympy/test_caching.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import logging +import os + +import pytest +import sympy as sp +from _pytest.logging import LogCaptureFixture + +from ampform.helicity import HelicityModel +from ampform.sympy import _warn_about_unsafe_hash, get_readable_hash + + +@pytest.mark.parametrize( + ("assumptions", "expected_hash"), + [ + # pylint: disable=use-dict-literal + (dict(), "pythonhashseed-0+7459658071388516764"), + (dict(real=True), "pythonhashseed-0+3665410414623666716"), + (dict(rational=True), "pythonhashseed-0-7926839224244779605"), + ], +) +def test_get_readable_hash(assumptions, expected_hash, caplog: LogCaptureFixture): + caplog.set_level(logging.WARNING) + x, y = sp.symbols("x y", **assumptions) + expr = x**2 + y + h = get_readable_hash(expr) + python_hash_seed = os.environ.get("PYTHONHASHSEED") + if python_hash_seed is None or not python_hash_seed.isdigit(): + assert h[:7] == "bbc9833" + # pylint: disable=too-many-function-args + if _warn_about_unsafe_hash.cache_info().hits == 0: + assert "PYTHONHASHSEED has not been set." in caplog.text + caplog.clear() + elif python_hash_seed == "0": + assert h == expected_hash + else: + pytest.skip("PYTHONHASHSEED has been set, but is not 0") + assert caplog.text == "" + + +def test_get_readable_hash_large(amplitude_model: tuple[str, HelicityModel]): + python_hash_seed = os.environ.get("PYTHONHASHSEED") + if python_hash_seed != "0": + pytest.skip("PYTHONHASHSEED is not 0") + formalism, model = amplitude_model + expected_hash = { + "helicity": "pythonhashseed-0+7234046307916118541", + "canonical-helicity": "pythonhashseed-0+5410851290893792717", + }[formalism] + assert get_readable_hash(model.expression) == expected_hash diff --git a/tox.ini b/tox.ini index 0f9014cf0..c019b0d16 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,8 @@ allowlist_externals = pytest commands = pytest {posargs} +setenv = + PYTHONHASHSEED = 0 [testenv:cov] description = @@ -27,6 +29,8 @@ commands = --cov-report=html \ --cov-report=xml \ --cov=ampform +setenv = + PYTHONHASHSEED = 0 [testenv:doc] description = @@ -45,6 +49,9 @@ passenv = GITHUB_REPO GITHUB_TOKEN READTHEDOCS_VERSION + TERM +setenv = + PYTHONHASHSEED = 0 [testenv:doclive] description = @@ -80,6 +87,8 @@ passenv = GITHUB_TOKEN READTHEDOCS_VERSION TERM +setenv = + PYTHONHASHSEED = 0 [testenv:docnb] description = @@ -97,8 +106,10 @@ passenv = GITHUB_REPO GITHUB_TOKEN READTHEDOCS_VERSION + TERM setenv = EXECUTE_NB = yes + PYTHONHASHSEED = 0 [testenv:docnb-force] description = @@ -147,6 +158,8 @@ commands = pytest --nbmake {posargs:docs} passenv = EXECUTE_NB +setenv = + PYTHONHASHSEED = 0 [testenv:pydeps] description = From 83c78c60c9a11cfcfc89f771b1e8e6da81aa8e53 Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Tue, 18 Oct 2022 17:55:49 +0200 Subject: [PATCH 05/13] MAINT: add failing `get_readable_hash` test for `EnergyDependentWidth` --- tests/sympy/test_caching.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/sympy/test_caching.py b/tests/sympy/test_caching.py index 1729a0d1c..2a06852a2 100644 --- a/tests/sympy/test_caching.py +++ b/tests/sympy/test_caching.py @@ -7,6 +7,7 @@ import sympy as sp from _pytest.logging import LogCaptureFixture +from ampform.dynamics import EnergyDependentWidth from ampform.helicity import HelicityModel from ampform.sympy import _warn_about_unsafe_hash, get_readable_hash @@ -39,6 +40,25 @@ def test_get_readable_hash(assumptions, expected_hash, caplog: LogCaptureFixture assert caplog.text == "" +def test_get_readable_hash_energy_dependent_width(): + angular_momentum = sp.Symbol("L", integer=True) + s, m0, w0, m_a, m_b, d = sp.symbols("s m0 Gamma0 m_a m_b d", nonnegative=True) + expr = EnergyDependentWidth( + s=s, + mass0=m0, + gamma0=w0, + m_a=m_a, + m_b=m_b, + angular_momentum=angular_momentum, + meson_radius=d, + ) + h = get_readable_hash(expr) + python_hash_seed = os.environ.get("PYTHONHASHSEED") + if python_hash_seed is None: + pytest.skip("PYTHONHASHSEED has been set, but is not 0") + assert h == "pythonhashseed-0+5847558977249966029" + + def test_get_readable_hash_large(amplitude_model: tuple[str, HelicityModel]): python_hash_seed = os.environ.get("PYTHONHASHSEED") if python_hash_seed != "0": From a636271a43440a4dcb76b764f10fb5570c99e32c Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Tue, 18 Oct 2022 18:03:06 +0200 Subject: [PATCH 06/13] FIX: implement stable hashing for `EnergyDependentWidth` --- src/ampform/dynamics/__init__.py | 3 ++- src/ampform/sympy/__init__.py | 5 +++++ tests/sympy/test_caching.py | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ampform/dynamics/__init__.py b/src/ampform/dynamics/__init__.py index 640ede5b1..3e3adabb2 100644 --- a/src/ampform/dynamics/__init__.py +++ b/src/ampform/dynamics/__init__.py @@ -197,7 +197,8 @@ def __getnewargs_ex__(self) -> tuple[tuple, dict]: def _hashable_content(self) -> tuple: # https://github.com/sympy/sympy/blob/1.10/sympy/core/basic.py#L157-L165 - return (*self.args, self.phsp_factor, self._name) + # phsp_factor is converted to string because of unstable hash for classes + return (*super()._hashable_content(), str(self.phsp_factor)) def evaluate(self) -> sp.Expr: s, mass0, gamma0, m_a, m_b, angular_momentum, meson_radius = self.args diff --git a/src/ampform/sympy/__init__.py b/src/ampform/sympy/__init__.py index bf539d2fb..575d5763b 100644 --- a/src/ampform/sympy/__init__.py +++ b/src/ampform/sympy/__init__.py @@ -103,6 +103,11 @@ def __getnewargs_ex__(self) -> tuple[tuple, dict]: kwargs = {"name": self._name} return args, kwargs + def _hashable_content(self) -> tuple: + # https://github.com/sympy/sympy/blob/1.10/sympy/core/basic.py#L157-L165 + # name is converted to string because unstable hash for None + return (*super()._hashable_content(), str(self._name)) + @abstractmethod def evaluate(self) -> sp.Expr: """Evaluate and 'unfold' this `UnevaluatedExpression` by one level. diff --git a/tests/sympy/test_caching.py b/tests/sympy/test_caching.py index 2a06852a2..f90d0c7f1 100644 --- a/tests/sympy/test_caching.py +++ b/tests/sympy/test_caching.py @@ -65,7 +65,7 @@ def test_get_readable_hash_large(amplitude_model: tuple[str, HelicityModel]): pytest.skip("PYTHONHASHSEED is not 0") formalism, model = amplitude_model expected_hash = { - "helicity": "pythonhashseed-0+7234046307916118541", - "canonical-helicity": "pythonhashseed-0+5410851290893792717", + "canonical-helicity": "pythonhashseed-0-7143983882032045549", + "helicity": "pythonhashseed-0+3357246175053927117", }[formalism] assert get_readable_hash(model.expression) == expected_hash From c3fdc39fa65c9e0ac66d06be40c382393ea8bade Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Tue, 18 Oct 2022 23:19:20 +0200 Subject: [PATCH 07/13] FIX: set `PYTHONHASHSEED` in test action --- .github/workflows/ci-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 8fbb664c6..0171b2072 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -1,4 +1,6 @@ name: pytest +env: + PYTHONHASHSEED: "0" on: push: From ea10a6b4e92af32f4b39e5f7e9ed4e0bac37346a Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Tue, 18 Oct 2022 23:27:24 +0200 Subject: [PATCH 08/13] DOC: show expression unfolding with `perform_cached_doit` --- docs/usage/amplitude.ipynb | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/usage/amplitude.ipynb b/docs/usage/amplitude.ipynb index b271d8473..85a6ffae6 100644 --- a/docs/usage/amplitude.ipynb +++ b/docs/usage/amplitude.ipynb @@ -644,6 +644,34 @@ " model = pickle.load(stream)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cached expression 'unfolding'\n", + "\n", + "Amplitude model expressions can be extremely large. AmpForm can formulate such expressions relatively fast, but {mod}`sympy` has to 'unfold' these expressions with {meth}`~sympy.core.basic.Basic.doit`, which can take a long time. AmpForm provides a function that can cache the 'unfolded' expression to disk, so that the expression unfolding runs faster upon the next run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ampform.sympy import perform_cached_doit\n", + "\n", + "full_expression = perform_cached_doit(model.expression)\n", + "sp.count_ops(full_expression)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "See {meth}`.perform_cached_doit` for some tips on how to improve performance." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -1035,8 +1063,16 @@ "name": "python3" }, "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", "name": "python", - "version": "3.8.12" + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.13" } }, "nbformat": 4, From 4c9e1ee47bb679d0167cc7d8462223c317ef8c63 Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Tue, 18 Oct 2022 23:35:14 +0200 Subject: [PATCH 09/13] FIX: use `func` instead of `meth` --- docs/usage/amplitude.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/amplitude.ipynb b/docs/usage/amplitude.ipynb index 85a6ffae6..a94128925 100644 --- a/docs/usage/amplitude.ipynb +++ b/docs/usage/amplitude.ipynb @@ -669,7 +669,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "See {meth}`.perform_cached_doit` for some tips on how to improve performance." + "See {func}`.perform_cached_doit` for some tips on how to improve performance." ] }, { From c3a804cff1e6c245ae1d6e7f3d8e07a16195696e Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Tue, 18 Oct 2022 23:43:11 +0200 Subject: [PATCH 10/13] FIX: make tests runnable on macOS https://github.com/ComPWA/ampform/actions/runs/3277058875/jobs/5393849802 --- tests/sympy/test_caching.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/sympy/test_caching.py b/tests/sympy/test_caching.py index f90d0c7f1..ca78eb6bd 100644 --- a/tests/sympy/test_caching.py +++ b/tests/sympy/test_caching.py @@ -22,6 +22,8 @@ ], ) def test_get_readable_hash(assumptions, expected_hash, caplog: LogCaptureFixture): + if os.environ.get("RUNNER_OS") == "macOS": + pytest.skip("Cannot run this test on macOS") caplog.set_level(logging.WARNING) x, y = sp.symbols("x y", **assumptions) expr = x**2 + y @@ -41,6 +43,8 @@ def test_get_readable_hash(assumptions, expected_hash, caplog: LogCaptureFixture def test_get_readable_hash_energy_dependent_width(): + if os.environ.get("RUNNER_OS") == "macOS": + pytest.skip("Cannot run this test on macOS") angular_momentum = sp.Symbol("L", integer=True) s, m0, w0, m_a, m_b, d = sp.symbols("s m0 Gamma0 m_a m_b d", nonnegative=True) expr = EnergyDependentWidth( @@ -64,8 +68,15 @@ def test_get_readable_hash_large(amplitude_model: tuple[str, HelicityModel]): if python_hash_seed != "0": pytest.skip("PYTHONHASHSEED is not 0") formalism, model = amplitude_model - expected_hash = { - "canonical-helicity": "pythonhashseed-0-7143983882032045549", - "helicity": "pythonhashseed-0+3357246175053927117", - }[formalism] + if os.environ.get("RUNNER_OS") == "macOS": + # https://github.com/ComPWA/ampform/actions/runs/3277058875/jobs/5393849802 + expected_hash = { + "canonical-helicity": "pythonhashseed-0-6040455869260657745", + "helicity": "pythonhashseed-0-1928646339459384503", + }[formalism] + else: + expected_hash = { + "canonical-helicity": "pythonhashseed-0-7143983882032045549", + "helicity": "pythonhashseed-0+3357246175053927117", + }[formalism] assert get_readable_hash(model.expression) == expected_hash From 4527da945ab924f82016bd56e9d281c8b2cb09d4 Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Tue, 18 Oct 2022 23:45:02 +0200 Subject: [PATCH 11/13] TEMP: set `PYTHONHASHSEED` explicity in `pytest-cov` GitHub job --- .github/workflows/ci-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 0171b2072..29279657d 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,6 +35,8 @@ jobs: pip install -c .constraints/py${{ matrix.python-version }}.txt -e .[test] - name: Test with pytest-cov run: pytest --cov=ampform --cov-report=xml + env: + PYTHONHASHSEED: "0" - uses: actions/upload-artifact@v3 if: ${{ always() }} with: From f45dd2d70a0c77b89c9d8eff7f7430f41a4f6c9e Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Tue, 18 Oct 2022 23:49:04 +0200 Subject: [PATCH 12/13] Revert "TEMP: set `PYTHONHASHSEED` explicity in `pytest-cov` GitHub job" This reverts commit 4527da945ab924f82016bd56e9d281c8b2cb09d4. --- .github/workflows/ci-tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 29279657d..0171b2072 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,8 +35,6 @@ jobs: pip install -c .constraints/py${{ matrix.python-version }}.txt -e .[test] - name: Test with pytest-cov run: pytest --cov=ampform --cov-report=xml - env: - PYTHONHASHSEED: "0" - uses: actions/upload-artifact@v3 if: ${{ always() }} with: From 0a66318a50c8ae4ebbd91b056fe7960d6b997f8d Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Tue, 18 Oct 2022 23:58:33 +0200 Subject: [PATCH 13/13] FIX: make it possible to run caching tests on Python 3.7 --- tests/sympy/test_caching.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/sympy/test_caching.py b/tests/sympy/test_caching.py index ca78eb6bd..9f34c07f9 100644 --- a/tests/sympy/test_caching.py +++ b/tests/sympy/test_caching.py @@ -2,6 +2,7 @@ import logging import os +import sys import pytest import sympy as sp @@ -22,8 +23,8 @@ ], ) def test_get_readable_hash(assumptions, expected_hash, caplog: LogCaptureFixture): - if os.environ.get("RUNNER_OS") == "macOS": - pytest.skip("Cannot run this test on macOS") + if sys.version_info < (3, 8): + pytest.skip("Cannot run this test on Python 3.7") caplog.set_level(logging.WARNING) x, y = sp.symbols("x y", **assumptions) expr = x**2 + y @@ -43,8 +44,6 @@ def test_get_readable_hash(assumptions, expected_hash, caplog: LogCaptureFixture def test_get_readable_hash_energy_dependent_width(): - if os.environ.get("RUNNER_OS") == "macOS": - pytest.skip("Cannot run this test on macOS") angular_momentum = sp.Symbol("L", integer=True) s, m0, w0, m_a, m_b, d = sp.symbols("s m0 Gamma0 m_a m_b d", nonnegative=True) expr = EnergyDependentWidth( @@ -60,7 +59,10 @@ def test_get_readable_hash_energy_dependent_width(): python_hash_seed = os.environ.get("PYTHONHASHSEED") if python_hash_seed is None: pytest.skip("PYTHONHASHSEED has been set, but is not 0") - assert h == "pythonhashseed-0+5847558977249966029" + if sys.version_info < (3, 8): + assert h == "pythonhashseed-0+6939334787254793397" + else: + assert h == "pythonhashseed-0+5847558977249966029" def test_get_readable_hash_large(amplitude_model: tuple[str, HelicityModel]): @@ -68,8 +70,9 @@ def test_get_readable_hash_large(amplitude_model: tuple[str, HelicityModel]): if python_hash_seed != "0": pytest.skip("PYTHONHASHSEED is not 0") formalism, model = amplitude_model - if os.environ.get("RUNNER_OS") == "macOS": + if sys.version_info < (3, 8): # https://github.com/ComPWA/ampform/actions/runs/3277058875/jobs/5393849802 + # https://github.com/ComPWA/ampform/actions/runs/3277143883/jobs/5394043014 expected_hash = { "canonical-helicity": "pythonhashseed-0-6040455869260657745", "helicity": "pythonhashseed-0-1928646339459384503",