diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 5eea6fa..0000000 --- a/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -ignore = E121,E123,E126,E226,E24,E704,W503,W504,E203 -max-line-length = 199 \ No newline at end of file diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index 621c7cd..74a7e34 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -3,66 +3,55 @@ name: PR Tests on: pull_request: branches: - - master + - master jobs: - linter: - name: Linter + pre-commit: + name: Format runs-on: ubuntu-latest - steps: - - name: Check out Git repository - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 with: - python-version: 3.9 - - - name: Install Python dependencies - run: pip install black flake8 - - - name: Run black - uses: wearerequired/lint-action@v2 + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - uses: pre-commit/action@v3.0.1 with: - auto_fix: true - black: true - black_auto_fix: true - - - name: Lint with flake8 - run: flake8 rhalphalib --count --show-source --statistics + extra_args: --hook-stage manual --all-files test: - name: Run pytest (Python ${{ matrix.python-version }}, ROOT ${{ matrix.root-version }}) - needs: linter + name: + Run pytest (Python ${{ matrix.python-version }}, ROOT ${{ + matrix.root-version }}) + needs: pre-commit runs-on: ubuntu-latest strategy: max-parallel: 4 fail-fast: true matrix: - python-version: [3.7] - root-version: [6.16, 6.22] + python-version: ["3.10"] + root-version: ["6.30.04", "6.32.10"] + include: + - python-version: "3.9" + root-version: "6.22.8" steps: - - uses: actions/checkout@v1 - - name: Set up Conda - uses: conda-incubator/setup-miniconda@v2.2.0 - with: - python-version: ${{ matrix.python-version }} - miniforge-variant: Mambaforge - channels: conda-forge,defaults - channel-priority: true - activate-environment: condaenv - - name: Install ROOT ${{ matrix.root-version }} - shell: bash -l {0} - run: | - mamba install -c conda-forge numpy scipy nomkl root==${{ matrix.root-version }} - - name: Install package - shell: bash -l {0} - run: | - pip install -e . - - name: Test with pytest - shell: bash -l {0} - run: | - pip install pytest - pytest tests + - uses: actions/checkout@v1 + - name: Set up Conda + uses: conda-incubator/setup-miniconda@v3 + with: + python-version: ${{ matrix.python-version }} + miniforge-version: latest + - name: Install ROOT ${{ matrix.root-version }} + shell: bash -l {0} + run: | + mamba install -c conda-forge numpy scipy nomkl root==${{ matrix.root-version }} + - name: Install package + shell: bash -l {0} + run: | + pip install .[test] + - name: Test with pytest + shell: bash -l {0} + run: | + pytest tests diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index 2d7a421..744731c 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -8,19 +8,19 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.gitignore b/.gitignore index d8acc33..5a0ef31 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ __pycache__ *.egg-info build dist -.vscode \ No newline at end of file +.vscode diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7875aae..cebcdc9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,80 @@ +ci: + autoupdate_commit_msg: "chore: update pre-commit hooks" + autofix_commit_msg: "style: pre-commit fixes" + +exclude: ^.cruft.json|.copier-answers.yml$ + repos: + - repo: https://github.com/adamchainz/blacken-docs + rev: "1.19.1" + hooks: + - id: blacken-docs + additional_dependencies: [black==24.*] + - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.0.0 + rev: "v5.0.0" + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + - id: name-tests-test + args: ["--pytest-test-first"] + - id: requirements-txt-fixer + - id: trailing-whitespace + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: "v1.10.0" + hooks: + - id: rst-backticks + - id: rst-directive-colons + - id: rst-inline-touching-normal + + - repo: https://github.com/rbubley/mirrors-prettier + rev: "v3.4.2" + hooks: + - id: prettier + types_or: [yaml, markdown, html, css, scss, javascript, json] + args: [--prose-wrap=always] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.9.2" + hooks: + - id: ruff + args: ["--fix", "--show-fixes"] + - id: ruff-format + + - repo: https://github.com/codespell-project/codespell + rev: "v2.3.0" + hooks: + - id: codespell + + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: "v0.10.0.1" + hooks: + - id: shellcheck + + - repo: local + hooks: + - id: disallow-caps + name: Disallow improper capitalization + language: pygrep + entry: PyBind|Numpy|Cmake|CCache|Github|PyTest + exclude: .pre-commit-config.yaml + + - repo: https://github.com/abravalheri/validate-pyproject + rev: "v0.23" hooks: - - id: flake8 - - repo: https://github.com/psf/black-pre-commit-mirror - rev: "23.10.1" + - id: validate-pyproject + additional_dependencies: ["validate-pyproject-schema-store[all]"] + + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: "0.31.0" hooks: - - id: black-jupyter \ No newline at end of file + - id: check-dependabot + - id: check-github-workflows + - id: check-readthedocs diff --git a/README.md b/README.md index 9f9b03f..07c5feb 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,15 @@ ## Quickstart -First, install [Combine v9](https://cms-analysis.github.io/HiggsAnalysis-CombinedLimit/#installation-instructions) using -your choice of installation instructions (with CMSSW, using LCG, or inside a Conda environment), then in that environment, run: +First, install +[Combine v9](https://cms-analysis.github.io/HiggsAnalysis-CombinedLimit/#installation-instructions) +using your choice of installation instructions (with CMSSW, using LCG, or inside +a Conda environment), then in that environment, run: + ```bash python3 -m pip install --user https://github.com/nsmith-/rhalphalib/archive/master.zip ``` -Take a look at [test_rhalphalib.py](https://github.com/nsmith-/rhalphalib/blob/master/tests/test_rhalphalib.py) + +Take a look at +[test_rhalphalib.py](https://github.com/nsmith-/rhalphalib/blob/master/tests/test_rhalphalib.py) for examples of how to use the package. diff --git a/pyproject.toml b/pyproject.toml index 8130e39..a5ef886 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,58 @@ -[tool.black] -line-length = 199 \ No newline at end of file +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "rhalphalib" +dynamic = ["version"] +authors = [ + {name = "Nick Smith", email = "nick.smith@cern.ch"}, +] +maintainers = [ + {name = "Nick Smith", email = "nick.smith@cern.ch"}, +] +description = "A binned fit intermediate representation library" +readme = "README.md" +license = {text = "BSD-3-Clause"} +# keywords = [] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering :: Physics", +] +requires-python = ">=3.8" +dependencies = [ + "numpy >=1.14", + "scipy", + "mplhep", +] + +[project.optional-dependencies] +test = [ + "pytest", +] +dev = [ + "pre-commit", +] +docs = [ + "sphinx>=7.0", + "myst_parser>=0.13", + "sphinx_copybutton", + "sphinx_autodoc_typehints", + "furo>=2023.08.17", +] + +[project.urls] +Homepage = "https://github.com/nsmith-/rhalphalib" + +[tool.setuptools_scm] +write_to = "src/rhalphalib/version.py" + +[tool.ruff] +line-length = 199 + +[tool.codespell] +ignore-words-list = "fpt" diff --git a/rhalphalib/version.py b/rhalphalib/version.py deleted file mode 100644 index d3ec452..0000000 --- a/rhalphalib/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.2.0" diff --git a/setup.py b/setup.py index a4534c2..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,49 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import sys -import os.path -from setuptools import ( - setup, - find_packages, -) +from setuptools import setup - -about = {} -with open(os.path.join("rhalphalib", "version.py")) as f: - exec(f.read(), about) - - -needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv) -pytest_runner = ["pytest-runner"] if needs_pytest else [] - -setup( - name="rhalphalib", - version=about["__version__"], - packages=find_packages(), - scripts=[], - include_package_data=True, - description="A binned fit intermediate representation library", - long_description=open("README.md", "rb").read().decode("utf8", "ignore"), - long_description_content_type="text/markdown", - maintainer="Nick Smith", - maintainer_email="nick.smith@cern.ch", - url="https://github.com/nsmith-/rhalphalib", - download_url="https://github.com/nsmith-/rhalphalib/releases", - license="BSD 3-clause", - test_suite="tests", - install_requires=[ - "numpy>=1.14", - "scipy", - ], - setup_requires=["flake8"] + pytest_runner, - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Topic :: Scientific/Engineering :: Physics", - ], -) +setup() diff --git a/rhalphalib/__init__.py b/src/rhalphalib/__init__.py similarity index 100% rename from rhalphalib/__init__.py rename to src/rhalphalib/__init__.py diff --git a/rhalphalib/function.py b/src/rhalphalib/function.py similarity index 100% rename from rhalphalib/function.py rename to src/rhalphalib/function.py diff --git a/rhalphalib/model.py b/src/rhalphalib/model.py similarity index 97% rename from rhalphalib/model.py rename to src/rhalphalib/model.py index 9b8314d..82a1ab7 100644 --- a/rhalphalib/model.py +++ b/src/rhalphalib/model.py @@ -99,8 +99,13 @@ def renderRoofit(self, workspace): workspace.add(rooSimul) rooObservable = ROOT.RooArgList(channel.observable.renderRoofit(workspace)) - # that's right I don't need no CombDataSetFactory - rooData = ROOT.RooDataHist(self.name + "_observation", "Combined observation", rooObservable, channelCat, obsmap) + rooData = ROOT.RooDataHist( + self.name + "_observation", + "Combined observation", + rooObservable, + ROOT.RooFit.Index(channelCat), + ROOT.RooFit.Import(obsmap), + ) workspace.add(rooData) elif rooSimul == None or rooData == None: # noqa: E711 raise RuntimeError("Model %r has a pdf or dataset already embedded in workspace %r" % (self, workspace)) @@ -168,7 +173,7 @@ def addSample(self, sample): if sample.name in self._samples: raise ValueError("Channel %r already has a sample named %s" % (self, sample.name)) if sample.name[: sample.name.find("_")] != self.name: - raise ValueError("Naming convention requires begining of sample %r name to be %s" % (sample, self.name)) + raise ValueError("Naming convention requires beginning of sample %r name to be %s" % (sample, self.name)) if self._observable is not None: if not sample.observable == self._observable: raise ValueError("Sample %r has an incompatible observable with channel %r" % (sample, self)) diff --git a/rhalphalib/parameter.py b/src/rhalphalib/parameter.py similarity index 98% rename from rhalphalib/parameter.py rename to src/rhalphalib/parameter.py index 233b982..b5359bf 100644 --- a/rhalphalib/parameter.py +++ b/src/rhalphalib/parameter.py @@ -73,6 +73,7 @@ def _binary_op(self, opinfo, other): out.intermediate = True return out elif isinstance(other, numbers.Number): + other = float(other) # cast np.float to float if right: name = type(other).__name__ + opname + self.name out = DependentParameter(name, "%r%s{0}" % (other, op), self) @@ -297,7 +298,7 @@ class Observable(Parameter): """ A simple struct that holds the name of an observable (e.g. x axis of discriminator histogram) and its binning The first sample attached to a channel will dictate how the rendering of the observable is done. - Subequent samples attached will be checked against the first, and if they match, their observable will be set + Subsequent samples attached will be checked against the first, and if they match, their observable will be set to the first samples' instance of this class. """ diff --git a/rhalphalib/plot/__init__.py b/src/rhalphalib/plot/__init__.py similarity index 100% rename from rhalphalib/plot/__init__.py rename to src/rhalphalib/plot/__init__.py diff --git a/rhalphalib/plot/input_shapes.py b/src/rhalphalib/plot/input_shapes.py similarity index 100% rename from rhalphalib/plot/input_shapes.py rename to src/rhalphalib/plot/input_shapes.py diff --git a/rhalphalib/plot/plot_TF.py b/src/rhalphalib/plot/plot_TF.py similarity index 100% rename from rhalphalib/plot/plot_TF.py rename to src/rhalphalib/plot/plot_TF.py diff --git a/rhalphalib/plot/plot_cov.py b/src/rhalphalib/plot/plot_cov.py similarity index 100% rename from rhalphalib/plot/plot_cov.py rename to src/rhalphalib/plot/plot_cov.py diff --git a/rhalphalib/sample.py b/src/rhalphalib/sample.py similarity index 97% rename from rhalphalib/sample.py rename to src/rhalphalib/sample.py index cf077d8..b8a4283 100644 --- a/rhalphalib/sample.py +++ b/src/rhalphalib/sample.py @@ -208,8 +208,9 @@ def setParamEffect(self, param, effect_up, effect_down=None, scale=None): _weighted_effect_magnitude = np.sum(abs(effect_up - 1) * self._nominal) / np.sum(self._nominal) if "shape" in param.combinePrior and _weighted_effect_magnitude > 0.5: print( - "effect_up ({}, {}) has magnitude greater than 50% ({:.2f}%), " - "you might be passing absolute values instead of relative".format(param.name, self._name, _weighted_effect_magnitude * 100) + "effect_up ({}, {}) has magnitude greater than 50% ({:.2f}%), you might be passing absolute values instead of relative".format( + param.name, self._name, _weighted_effect_magnitude * 100 + ) ) self._paramEffectsUp[param] = effect_up @@ -237,8 +238,9 @@ def setParamEffect(self, param, effect_up, effect_down=None, scale=None): _weighted_effect_magnitude = np.sum(abs(effect_down - 1) * self._nominal) / np.sum(self._nominal) if "shape" in param.combinePrior and _weighted_effect_magnitude > 0.5: print( - "effect_down ({}, {}) has magnitude greater than 50% ({:.2f}%), " - "you might be passing absolute values instead of relative".format(param.name, self._name, _weighted_effect_magnitude * 100) + "effect_down ({}, {}) has magnitude greater than 50% ({:.2f}%), you might be passing absolute values instead of relative".format( + param.name, self._name, _weighted_effect_magnitude * 100 + ) ) self._paramEffectsDown[param] = effect_down else: @@ -259,7 +261,7 @@ def getParamEffect(self, param, up=True): return self._paramEffectsUp[param] else: if param not in self._paramEffectsDown or self._paramEffectsDown[param] is None: - # TODO the symmeterized value depends on if param prior is 'shapeN' or 'shape' + # TODO the symmetrized value depends on if param prior is 'shapeN' or 'shape' if param.combinePrior == "lnN": return 1.0 / self._paramEffectsUp[param] elif param.combinePrior == "shape": @@ -380,7 +382,7 @@ def getExpectation(self, nominal=False, eval=False): def renderRoofit(self, workspace): """ Import the necessary Roofit objects into the workspace for this sample - and return an extended pdf representing this sample's prediciton for pdf and norm. + and return an extended pdf representing this sample's prediction for pdf and norm. """ import ROOT @@ -451,7 +453,7 @@ def combineParamEffect(self, param): up = (self.getParamEffect(param, up=True) - 1) * scale + 1 down = (self.getParamEffect(param, up=False) - 1) * scale + 1 if isinstance(up, np.ndarray): - # Convert shape to norm (note symmeterized effect on shape != symmeterized effect on norm) + # Convert shape to norm (note symmetrized effect on shape != symmetrized effect on norm) nominal = self.getExpectation(nominal=True) if nominal.sum() == 0: up = 1.0 @@ -460,7 +462,7 @@ def combineParamEffect(self, param): up = (up * nominal).sum() / nominal.sum() down = (down * nominal).sum() / nominal.sum() elif self._paramEffectsDown[param] is None: - # Here we can safely defer to combine to calculate symmeterized effect + # Here we can safely defer to combine to calculate symmetrized effect down = None if down is None: return "%.4f" % up @@ -544,7 +546,7 @@ def getParamEffect(self, param, up=True): return self._paramEffectsUp[param] else: if self._paramEffectsDown[param] is None: - # TODO the symmeterized value depends on if param prior is 'shapeN' or 'shape' + # TODO the symmetrized value depends on if param prior is 'shapeN' or 'shape' return 1.0 / self._paramEffectsUp[param] return self._paramEffectsDown[param] @@ -635,7 +637,7 @@ def renderRoofit(self, workspace): def combineNormalization(self): """ - For combine, the normalization in the card is used to scale the parameteric process PDF + For combine, the normalization in the card is used to scale the parametric process PDF Since we provide an explicit normalization function, this should always stay at 1. """ # TODO: optionally we could set the normalization here and leave only normalization modifiers diff --git a/rhalphalib/template_morph.py b/src/rhalphalib/template_morph.py similarity index 100% rename from rhalphalib/template_morph.py rename to src/rhalphalib/template_morph.py diff --git a/rhalphalib/util.py b/src/rhalphalib/util.py similarity index 100% rename from rhalphalib/util.py rename to src/rhalphalib/util.py