diff --git a/.github/workflows/release_tests.yml b/.github/workflows/release_tests.yml index df1f6d66e7..c161badf75 100644 --- a/.github/workflows/release_tests.yml +++ b/.github/workflows/release_tests.yml @@ -34,16 +34,17 @@ jobs: - name: Install from PyPI run: | python -m pip install --upgrade pip setuptools wheel - python -m pip install pyhf[backends,xmlio] - python -m pip install 'pytest~=7.0' pytest-cov + python -m pip install --pre pyhf[backends,xmlio] + python -m pip install pytest pytest-cov python -m pip list - name: Canary test public API run: | pytest tests/test_public_api.py + # FIXME: c.f. https://github.com/proycon/codemetapy/issues/24 - name: Verify requirements in codemeta.json run: | - python -m pip install jq "codemetapy>=0.3.4" - codemetapy --no-extras pyhf > codemeta_generated.json - diff <(jq -S .softwareRequirements codemeta_generated.json) <(jq -S .softwareRequirements codemeta.json) + python -m pip install jq "codemetapy>=2.2.2" + codemetapy --inputtype python --no-extras pyhf > codemeta_generated.json + diff <(jq -S .softwareRequirements codemeta.json) <(jq -S .softwareRequirements codemeta_generated.json) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d44a93509..e706b3c0c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -58,6 +58,7 @@ repos: hooks: - id: flake8 args: ["--count", "--statistics"] + additional_dependencies: [flake8-encodings==0.5.0.post1] - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.971 diff --git a/.zenodo.json b/.zenodo.json index b8cb610161..958ae48464 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -1,8 +1,8 @@ { "description": "pure-Python HistFactory implementation with tensors and autodiff", "license": "Apache-2.0", - "title": "scikit-hep/pyhf: v0.7.0rc3", - "version": "v0.7.0rc3", + "title": "scikit-hep/pyhf: v0.7.0rc4", + "version": "v0.7.0rc4", "upload_type": "software", "creators": [ { @@ -36,7 +36,7 @@ "related_identifiers": [ { "scheme": "url", - "identifier": "https://github.com/scikit-hep/pyhf/tree/v0.7.0rc3", + "identifier": "https://github.com/scikit-hep/pyhf/tree/v0.7.0rc4", "relation": "isSupplementTo" } ] diff --git a/CITATION.cff b/CITATION.cff index c37eac3dab..bebde0a1be 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -14,11 +14,11 @@ authors: given-names: "Giordon" orcid: "https://orcid.org/0000-0001-6616-3433" affiliation: "SCIPP, University of California, Santa Cruz" -title: "pyhf: v0.7.0rc3" -version: 0.7.0rc3 +title: "pyhf: v0.7.0rc4" +version: 0.7.0rc4 doi: 10.5281/zenodo.1169739 -repository-code: "https://github.com/scikit-hep/pyhf/releases/tag/v0.7.0rc3" -url: "https://pyhf.readthedocs.io/en/v0.7.0rc3/" +repository-code: "https://github.com/scikit-hep/pyhf/releases/tag/v0.7.0rc4" +url: "https://pyhf.readthedocs.io/en/v0.7.0rc4/" keywords: - python - physics diff --git a/README.rst b/README.rst index bbddfa4192..e0a793aa5b 100644 --- a/README.rst +++ b/README.rst @@ -309,11 +309,11 @@ the preferred BibTeX entry for citation of ``pyhf`` includes both the @software{pyhf, author = {Lukas Heinrich and Matthew Feickert and Giordon Stark}, - title = "{pyhf: v0.7.0rc3}", - version = {0.7.0rc3}, + title = "{pyhf: v0.7.0rc4}", + version = {0.7.0rc4}, doi = {10.5281/zenodo.1169739}, url = {https://doi.org/10.5281/zenodo.1169739}, - note = {https://github.com/scikit-hep/pyhf/releases/tag/v0.7.0rc3} + note = {https://github.com/scikit-hep/pyhf/releases/tag/v0.7.0rc4} } @article{pyhf_joss, @@ -340,6 +340,7 @@ contributors `__. Milestones ---------- +- 2022-09-12: 2000 GitHub issues and pull requests. (See PR `#2000 `__) - 2021-12-09: 1000 commits to the project. (See PR `#1710 `__) - 2020-07-28: 1000 GitHub issues and pull requests. (See PR `#1000 `__) @@ -360,7 +361,7 @@ and grant `OAC-1450377 Physics" + ], + "audience": { + "@id": "/audience/science-research", + "@type": "Audience", + "audienceType": "Science/Research" + }, "author": [ { - "@id": "https://orcid.org/0000-0002-4048-7584", + "@id": "/person/lukas-heinrich", "@type": "Person", - "givenName": "Lukas", + "email": "lukas.heinrich@cern.ch", "familyName": "Heinrich", - "email": "lukas.heinrich@cern.ch" + "givenName": "Lukas", + "identifier": "https://orcid.org/0000-0002-4048-7584", + "position": 1 }, { - "@id": "https://orcid.org/0000-0003-4124-7862", + "@id": "/person/matthew-feickert", "@type": "Person", - "givenName": "Matthew", + "email": "matthew.feickert@cern.ch", "familyName": "Feickert", - "email": "matthew.feickert@cern.ch" + "givenName": "Matthew", + "identifier": "https://orcid.org/0000-0003-4124-7862", + "position": 2 }, { - "@id": "https://orcid.org/0000-0001-6616-3433", + "@id": "/person/giordon-stark", "@type": "Person", - "givenName": "Giordon", + "email": "gstark@cern.ch", "familyName": "Stark", - "email": "gstark@cern.ch" + "givenName": "Giordon", + "identifier": "https://orcid.org/0000-0001-6616-3433", + "position": 3 } ], + "codeRepository": "https://github.com/scikit-hep/pyhf", + "description": "pure-Python HistFactory implementation with tensors and autodiff", + "developmentStatus": "4 - Beta", + "identifier": "pyhf", + "issueTracker": "https://github.com/scikit-hep/pyhf/issues", + "keywords": "physics fitting numpy scipy tensorflow pytorch jax", + "license": "http://spdx.org/licenses/Apache-2.0", + "name": "pyhf", + "releaseNotes": "https://pyhf.readthedocs.io/en/stable/release-notes.html", + "runtimePlatform": [ + "Python 3", + "Python 3 Only", + "Python 3.10", + "Python 3.7", + "Python 3.8", + "Python 3.9", + "Python Implementation CPython" + ], + "softwareHelp": { + "@id": "https://pyhf.readthedocs.io/" + }, "softwareRequirements": [ { + "@id": "/dependency/click-ge-8.0.0", "@type": "SoftwareApplication", - "identifier": "scipy", - "name": "scipy", - "provider": { - "@id": "https://pypi.org", - "@type": "Organization", - "name": "The Python Package Index", - "url": "https://pypi.org" - }, + "identifier": "click", + "name": "click", "runtimePlatform": "Python 3", - "version": ">=1.4.1" + "version": ">=8.0.0" }, { + "@id": "/dependency/importlib-resources-ge-1.4.0", "@type": "SoftwareApplication", - "identifier": "click", - "name": "click", - "provider": { - "@id": "https://pypi.org", - "@type": "Organization", - "name": "The Python Package Index", - "url": "https://pypi.org" - }, + "identifier": "importlib-resources", + "name": "importlib-resources", "runtimePlatform": "Python 3", - "version": ">=7.0" + "version": ">=1.4.0" }, { + "@id": "/dependency/jsonpatch-ge-1.15", "@type": "SoftwareApplication", - "identifier": "tqdm", - "name": "tqdm", - "provider": { - "@id": "https://pypi.org", - "@type": "Organization", - "name": "The Python Package Index", - "url": "https://pypi.org" - }, + "identifier": "jsonpatch", + "name": "jsonpatch", "runtimePlatform": "Python 3", - "version": ">=4.56.0" + "version": ">=1.15" }, { + "@id": "/dependency/jsonschema-ge-4.15.0", "@type": "SoftwareApplication", "identifier": "jsonschema", "name": "jsonschema", - "provider": { - "@id": "https://pypi.org", - "@type": "Organization", - "name": "The Python Package Index", - "url": "https://pypi.org" - }, "runtimePlatform": "Python 3", - "version": ">=3.0.0" + "version": ">=4.15.0" }, { + "@id": "/dependency/pyyaml-ge-5.1", "@type": "SoftwareApplication", - "identifier": "jsonpatch", - "name": "jsonpatch", - "provider": { - "@id": "https://pypi.org", - "@type": "Organization", - "name": "The Python Package Index", - "url": "https://pypi.org" - }, + "identifier": "pyyaml", + "name": "pyyaml", "runtimePlatform": "Python 3", - "version": ">=1.15" + "version": ">=5.1" }, { + "@id": "/dependency/scipy-ge-1.1.0", "@type": "SoftwareApplication", - "identifier": "pyyaml", - "name": "pyyaml", - "provider": { - "@id": "https://pypi.org", - "@type": "Organization", - "name": "The Python Package Index", - "url": "https://pypi.org" - }, + "identifier": "scipy", + "name": "scipy", "runtimePlatform": "Python 3", - "version": ">=5.1" - } - ], - "audience": [ + "version": ">=1.1.0" + }, { - "@type": "Audience", - "audienceType": "Science/Research" + "@id": "/dependency/tqdm-ge-4.56.0", + "@type": "SoftwareApplication", + "identifier": "tqdm", + "name": "tqdm", + "runtimePlatform": "Python 3", + "version": ">=4.56.0" + }, + { + "@id": "/dependency/typing-extensions-ge-3.7.4.3", + "@type": "SoftwareApplication", + "identifier": "typing-extensions", + "name": "typing-extensions", + "runtimePlatform": "Python 3", + "version": ">=3.7.4.3" } ], - "provider": { - "@id": "https://pypi.org", - "@type": "Organization", - "name": "The Python Package Index", - "url": "https://pypi.org" + "targetProduct": { + "@id": "/commandlineapplication/pyhf", + "@type": "CommandLineApplication", + "description": "The pyhf command line interface.", + "executableName": "pyhf", + "name": "pyhf", + "runtimePlatform": "Python 3" }, - "runtimePlatform": "Python 3", "url": "https://github.com/scikit-hep/pyhf", - "keywords": "physics fitting numpy scipy tensorflow pytorch jax", - "developmentStatus": "4 - Beta", - "applicationCategory": "Scientific/Engineering, Scientific/Engineering :: Physics", - "programmingLanguage": "Python 3, Python 3.7, Python 3.8, Python 3.9, Python 3.10, Python Implementation CPython" + "version": "0.7.0rc4" } diff --git a/docs/api.rst b/docs/api.rst index 8b678b6e91..57e09b70d2 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -157,10 +157,19 @@ Fits and Tests mle.fit mle.fixed_poi_fit hypotest - intervals.upperlimit - intervals.upperlimit_auto utils.all_pois_floating +Confidence Intervals +~~~~~~~~~~~~~~~~~~~~ + +.. autosummary:: + :toctree: _generated/ + :nosignatures: + + intervals.upper_limits.upper_limit + intervals.upper_limits.toms748_scan + intervals.upper_limits.linear_grid_scan + intervals.upperlimit Schema ------ diff --git a/docs/contributors.rst b/docs/contributors.rst index 9ee54b8578..a0da954d61 100644 --- a/docs/contributors.rst +++ b/docs/contributors.rst @@ -29,3 +29,4 @@ Contributors include: - Aryan Roy - Jerry Ling - Nathan Simpson +- Beojan Stanislaus diff --git a/docs/development.rst b/docs/development.rst index 00ecb9dd0e..d3b54dbf6b 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -52,7 +52,7 @@ available by the ``datadir`` fixture. Therefore, one can do: .. code-block:: python def test_patchset(datadir): - data_file = open(datadir.join("test.txt")) + data_file = open(datadir.join("test.txt"), encoding="utf-8") ... which will load the copy of ``text.txt`` in the temporary directory. This also diff --git a/docs/examples/notebooks/ImpactPlot.ipynb b/docs/examples/notebooks/ImpactPlot.ipynb index 0d10ddcd4c..aa045c124f 100644 --- a/docs/examples/notebooks/ImpactPlot.ipynb +++ b/docs/examples/notebooks/ImpactPlot.ipynb @@ -76,9 +76,13 @@ "outputs": [], "source": [ "def make_model(channel_list):\n", - " spec = json.load(open(\"1Lbb-probability-models/RegionA/BkgOnly.json\"))\n", + " spec = json.load(\n", + " open(\"1Lbb-probability-models/RegionA/BkgOnly.json\", encoding=\"utf-8\")\n", + " )\n", " patchset = pyhf.PatchSet(\n", - " json.load(open(\"1Lbb-probability-models/RegionA/patchset.json\"))\n", + " json.load(\n", + " open(\"1Lbb-probability-models/RegionA/patchset.json\", encoding=\"utf-8\")\n", + " )\n", " )\n", " patch = patchset[\"sbottom_750_745_60\"]\n", " spec = jsonpatch.apply_patch(spec, patch)\n", diff --git a/docs/examples/notebooks/ShapeFactor.ipynb b/docs/examples/notebooks/ShapeFactor.ipynb index 7527fb9855..32274c94ab 100644 --- a/docs/examples/notebooks/ShapeFactor.ipynb +++ b/docs/examples/notebooks/ShapeFactor.ipynb @@ -176,7 +176,11 @@ } ], "source": [ - "obs_limit, exp_limits, (poi_tests, tests) = pyhf.infer.intervals.upperlimit(\n", + "(\n", + " obs_limit,\n", + " exp_limits,\n", + " (poi_tests, tests),\n", + ") = pyhf.infer.intervals.upper_limits.upper_limit(\n", " data, pdf, np.linspace(0, 5, 61), level=0.05, return_results=True\n", ")" ] diff --git a/docs/examples/notebooks/binderexample/StatisticalAnalysis.ipynb b/docs/examples/notebooks/binderexample/StatisticalAnalysis.ipynb index d777b43cde..42c3b4656d 100644 --- a/docs/examples/notebooks/binderexample/StatisticalAnalysis.ipynb +++ b/docs/examples/notebooks/binderexample/StatisticalAnalysis.ipynb @@ -2295,7 +2295,11 @@ "outputs": [], "source": [ "mu_tests = np.linspace(0, 1, 16)\n", - "obs_limit, exp_limits, (poi_tests, tests) = pyhf.infer.intervals.upperlimit(\n", + "(\n", + " obs_limit,\n", + " exp_limits,\n", + " (poi_tests, tests),\n", + ") = pyhf.infer.intervals.upper_limits.upper_limit(\n", " data, pdf, mu_tests, level=0.05, return_results=True\n", ")" ] diff --git a/docs/examples/notebooks/multiBinPois.ipynb b/docs/examples/notebooks/multiBinPois.ipynb index 5b58b28f7f..ec2a0c6b59 100644 --- a/docs/examples/notebooks/multiBinPois.ipynb +++ b/docs/examples/notebooks/multiBinPois.ipynb @@ -85,7 +85,7 @@ } ], "source": [ - "source = json.load(open(validation_datadir + '/1bin_example1.json'))\n", + "source = json.load(open(validation_datadir + \"/1bin_example1.json\", encoding=\"utf-8\"))\n", "model = uncorrelated_background(\n", " source['bindata']['sig'], source['bindata']['bkg'], source['bindata']['bkgerr']\n", ")\n", @@ -94,7 +94,11 @@ "init_pars = model.config.suggested_init()\n", "par_bounds = model.config.suggested_bounds()\n", "\n", - "obs_limit, exp_limits, (poi_tests, tests) = pyhf.infer.intervals.upperlimit(\n", + "(\n", + " obs_limit,\n", + " exp_limits,\n", + " (poi_tests, tests),\n", + ") = pyhf.infer.intervals.upper_limits.upper_limit(\n", " data, model, np.linspace(0, 5, 61), level=0.05, return_results=True\n", ")" ] diff --git a/docs/examples/notebooks/multichannel-coupled-histo.ipynb b/docs/examples/notebooks/multichannel-coupled-histo.ipynb index 60665951cf..3c565fd93f 100644 --- a/docs/examples/notebooks/multichannel-coupled-histo.ipynb +++ b/docs/examples/notebooks/multichannel-coupled-histo.ipynb @@ -165,7 +165,9 @@ } ], "source": [ - "with open(validation_datadir + \"/2bin_2channel_coupledhisto.json\") as spec:\n", + "with open(\n", + " validation_datadir + \"/2bin_2channel_coupledhisto.json\", encoding=\"utf-8\"\n", + ") as spec:\n", " source = json.load(spec)\n", "\n", "data, pdf = prep_data(source[\"channels\"])\n", diff --git a/docs/examples/notebooks/pullplot.ipynb b/docs/examples/notebooks/pullplot.ipynb index ca76f61fd0..a259ba2cc9 100644 --- a/docs/examples/notebooks/pullplot.ipynb +++ b/docs/examples/notebooks/pullplot.ipynb @@ -72,7 +72,10 @@ "outputs": [], "source": [ "def make_model(channel_list):\n", - " spec = json.load(open(\"1Lbb-probability-models/RegionA/BkgOnly.json\"))\n", + " with open(\n", + " \"1Lbb-probability-models/RegionA/BkgOnly.json\", encoding=\"utf-8\"\n", + " ) as spec_file:\n", + " spec = json.load(spec_file)\n", " spec[\"channels\"] = [c for c in spec[\"channels\"] if c[\"name\"] in channel_list]\n", " spec[\"measurements\"][0][\"config\"][\"poi\"] = \"lumi\"\n", "\n", diff --git a/docs/faq.rst b/docs/faq.rst index 9644a7c006..d821139463 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -30,6 +30,46 @@ Use the :code:`--backend` option for :code:`pyhf cls` to specify a tensor backen The default backend is NumPy. For more information see :code:`pyhf cls --help`. +I installed an old ``pyhf`` release from PyPI, why am I getting an error from a dependency? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For old releases of ``pyhf`` that are not actively supported anymore you might +need to manually constrain the **upper bound** of a dependency. + +We work hard to make sure that ``pyhf`` is well maintained so that it installs +correctly "out of the box" and have tested all of ``pyhf``'s core dependencies +to determine hard lower bounds for compatible dependency releases. +However, as ``pyhf`` is |Henry Python library blog|_ we can only define lower +bounds for its core dependencies, as defining upper bounds would make decisions +for users on what versions of libraries they can use in Python applications they +build with ``pyhf`` --- |Hynek SemVer blog|_. +If ``pyhf`` were to define upper bounds we could create situations in which +``pyhf`` and other libraries defined in an environment file (i.e., +``requirements.txt``) could have directly conflicting dependencies that would +result in ``pip`` failing to be able to install ``pyhf``. + +To give an explicit example, |jsonschema GitHub Discussion 995|_ resulted in a +``KeyError`` if used with ``pyhf`` ``v0.6.3`` or older. +This problem was fixed (c.f. Pull Request :pr:`1979`) in the next release with +``pyhf`` ``v0.7.0``, but the intermediate solution for users was to install an +older version of ``jsonschema`` that was still compatible with the ``pyhf`` +release they were using: + + .. code-block:: + + # requirements.txt + pyhf==0.6.3 + jsonschema<4.15.0 + +.. |Henry Python library blog| replace:: a Python library +.. _`Henry Python library blog`: https://iscinumpy.dev/post/app-vs-library/ + +.. |Hynek SemVer blog| replace:: that would be bad +.. _`Hynek SemVer blog`: https://hynek.me/articles/semver-will-not-save-you/ + +.. |jsonschema GitHub Discussion 995| replace:: breaking changes in ``jsonschema`` ``v4.15.0``'s behavior +.. _`jsonschema GitHub Discussion 995`: https://github.com/orgs/python-jsonschema/discussions/995 + Does ``pyhf`` support Python 2? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ No. diff --git a/docs/generate_jupyterlite_iframe.py b/docs/generate_jupyterlite_iframe.py index 453a80c3ee..8528a14c34 100644 --- a/docs/generate_jupyterlite_iframe.py +++ b/docs/generate_jupyterlite_iframe.py @@ -4,7 +4,7 @@ def main(): code = """\ import piplite -await piplite.install(["pyhf==0.7.0rc3", "requests"]) +await piplite.install(["pyhf==0.7.0rc4", "requests"]) %matplotlib inline import pyhf\ """ diff --git a/docs/jupyterlite.rst b/docs/jupyterlite.rst index f3aef3dcca..77389b88d2 100644 --- a/docs/jupyterlite.rst +++ b/docs/jupyterlite.rst @@ -21,7 +21,7 @@ Try out now with JupyterLite_ .. raw:: html diff --git a/docs/likelihood.rst b/docs/likelihood.rst index b724a7da35..a3e687a6e2 100644 --- a/docs/likelihood.rst +++ b/docs/likelihood.rst @@ -28,7 +28,8 @@ check that it conforms to the provided workspace specification as follows: import json, requests, jsonschema - workspace = json.load(open("/path/to/analysis_workspace.json")) + with open("/path/to/analysis_workspace.json", encoding="utf-8") as ws_file: + workspace = json.load(ws_file) # if no exception is raised, it found and parsed the schema schema = requests.get("https://scikit-hep.org/pyhf/schemas/1.0.0/workspace.json").json() # If no exception is raised by validate(), the instance is valid. diff --git a/pyproject.toml b/pyproject.toml index 3fdaf65392..4214261f18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ filterwarnings = [ 'ignore:Creating a tensor from a list of numpy.ndarrays is extremely slow. Please consider converting the list to a single numpy.ndarray with:UserWarning', #FIXME: tests/test_optim.py::test_minimize[no_grad-scipy-pytorch-no_stitch] 'ignore:divide by zero encountered in (true_)?divide:RuntimeWarning', #FIXME: pytest tests/test_tensor.py::test_pdf_calculations[numpy] 'ignore:[A-Z]+ is deprecated and will be removed in Pillow 10:DeprecationWarning', # keras + 'ignore:Call to deprecated create function:DeprecationWarning', # protobuf via tensorflow ] [tool.nbqa.mutate] diff --git a/setup.cfg b/setup.cfg index e43033e997..3484d02c58 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ package_dir = include_package_data = True python_requires = >=3.7 install_requires = - scipy>=1.1.0 # requires numpy, which is required by pyhf and tensorflow + scipy>=1.2.0 # requires numpy, which is required by pyhf and tensorflow click>=8.0.0 # for console scripts tqdm>=4.56.0 # for readxml jsonschema>=4.15.0 # for utils diff --git a/src/pyhf/cli/infer.py b/src/pyhf/cli/infer.py index 5081d2d156..05894be405 100644 --- a/src/pyhf/cli/infer.py +++ b/src/pyhf/cli/infer.py @@ -99,10 +99,13 @@ def fit( ) set_backend(tensorlib, new_optimizer(**optconf)) - with click.open_file(workspace, "r") as specstream: + with click.open_file(workspace, "r", encoding="utf-8") as specstream: spec = json.load(specstream) ws = Workspace(spec) - patches = [json.loads(click.open_file(pfile, "r").read()) for pfile in patch] + patches = [ + json.loads(click.open_file(pfile, "r", encoding="utf-8").read()) + for pfile in patch + ] model = ws.model( measurement_name=measurement, @@ -125,7 +128,7 @@ def fit( if output_file is None: click.echo(json.dumps(result, indent=4, sort_keys=True)) else: - with open(output_file, "w+") as out_file: + with open(output_file, "w+", encoding="utf-8") as out_file: json.dump(result, out_file, indent=4, sort_keys=True) log.debug(f"Written to {output_file:s}") @@ -190,12 +193,15 @@ def cls( "CLs_obs": 0.3599845631401915 } """ - with click.open_file(workspace, 'r') as specstream: + with click.open_file(workspace, "r", encoding="utf-8") as specstream: spec = json.load(specstream) ws = Workspace(spec) - patches = [json.loads(click.open_file(pfile, 'r').read()) for pfile in patch] + patches = [ + json.loads(click.open_file(pfile, "r", encoding="utf-8").read()) + for pfile in patch + ] model = ws.model( measurement_name=measurement, patches=patches, @@ -241,6 +247,6 @@ def cls( if output_file is None: click.echo(json.dumps(result, indent=4, sort_keys=True)) else: - with open(output_file, 'w+') as out_file: + with open(output_file, "w+", encoding="utf-8") as out_file: json.dump(result, out_file, indent=4, sort_keys=True) log.debug(f"Written to {output_file:s}") diff --git a/src/pyhf/cli/patchset.py b/src/pyhf/cli/patchset.py index 50f5855cb8..75bdb56887 100644 --- a/src/pyhf/cli/patchset.py +++ b/src/pyhf/cli/patchset.py @@ -39,7 +39,7 @@ def extract(patchset, name, output_file, with_metadata): Returns: jsonpatch (:obj:`list`): A list of jsonpatch operations to apply to a workspace. """ - with click.open_file(patchset, 'r') as fstream: + with click.open_file(patchset, "r", encoding="utf-8") as fstream: patchset_spec = json.load(fstream) patchset = PatchSet(patchset_spec) @@ -52,7 +52,7 @@ def extract(patchset, name, output_file, with_metadata): result = patch.patch if output_file: - with open(output_file, 'w+') as out_file: + with open(output_file, "w", encoding="utf-8") as out_file: json.dump(result, out_file, indent=4, sort_keys=True) log.debug(f"Written to {output_file:s}") else: @@ -79,19 +79,19 @@ def apply(background_only, patchset, name, output_file): Returns: workspace (:class:`~pyhf.workspace.Workspace`): The patched background-only workspace. """ - with click.open_file(background_only, 'r') as specstream: + with click.open_file(background_only, "r", encoding="utf-8") as specstream: spec = json.load(specstream) ws = Workspace(spec) - with click.open_file(patchset, 'r') as fstream: + with click.open_file(patchset, "r", encoding="utf-8") as fstream: patchset_spec = json.load(fstream) patchset = PatchSet(patchset_spec) patched_ws = patchset.apply(ws, name) if output_file: - with open(output_file, 'w+') as out_file: + with open(output_file, "w+", encoding="utf-8") as out_file: json.dump(patched_ws, out_file, indent=4, sort_keys=True) log.debug(f"Written to {output_file:s}") else: @@ -111,12 +111,12 @@ def verify(background_only, patchset): Returns: None """ - with click.open_file(background_only, 'r') as specstream: + with click.open_file(background_only, "r", encoding="utf-8") as specstream: spec = json.load(specstream) ws = Workspace(spec) - with click.open_file(patchset, 'r') as fstream: + with click.open_file(patchset, "r", encoding="utf-8") as fstream: patchset_spec = json.load(fstream) patchset = PatchSet(patchset_spec) @@ -134,7 +134,7 @@ def inspect(patchset): Returns: None """ - with click.open_file(patchset, 'r') as fstream: + with click.open_file(patchset, "r", encoding="utf-8") as fstream: patchset_spec = json.load(fstream) patchset = PatchSet(patchset_spec) diff --git a/src/pyhf/cli/rootio.py b/src/pyhf/cli/rootio.py index 06411773f8..d1312c2641 100644 --- a/src/pyhf/cli/rootio.py +++ b/src/pyhf/cli/rootio.py @@ -65,7 +65,7 @@ def xml2json( if output_file is None: click.echo(json.dumps(spec, indent=4, sort_keys=True)) else: - with open(output_file, 'w+') as out_file: + with open(output_file, "w+", encoding="utf-8") as out_file: json.dump(spec, out_file, indent=4, sort_keys=True) log.debug(f"Written to {output_file:s}") @@ -92,15 +92,15 @@ def json2xml(workspace, output_dir, specroot, dataroot, resultprefix, patch): from pyhf import writexml os.makedirs(output_dir, exist_ok=True) - with click.open_file(workspace, 'r') as specstream: + with click.open_file(workspace, "r", encoding="utf-8") as specstream: spec = json.load(specstream) for pfile in patch: - patch = json.loads(click.open_file(pfile, 'r').read()) + patch = json.loads(click.open_file(pfile, "r", encoding="utf-8").read()) spec = jsonpatch.JsonPatch(patch).apply(spec) os.makedirs(Path(output_dir).joinpath(specroot), exist_ok=True) os.makedirs(Path(output_dir).joinpath(dataroot), exist_ok=True) with click.open_file( - Path(output_dir).joinpath(f'{resultprefix}.xml'), 'w' + Path(output_dir).joinpath(f"{resultprefix}.xml"), "w", encoding="utf-8" ) as outstream: outstream.write( writexml.writexml( diff --git a/src/pyhf/cli/spec.py b/src/pyhf/cli/spec.py index 47beac9e73..ad335851b8 100644 --- a/src/pyhf/cli/spec.py +++ b/src/pyhf/cli/spec.py @@ -60,7 +60,7 @@ def inspect(workspace, output_file, measurement): (*) Measurement mu (none) """ - with click.open_file(workspace, 'r') as specstream: + with click.open_file(workspace, "r", encoding="utf-8") as specstream: spec = json.load(specstream) ws = Workspace(spec) @@ -158,7 +158,7 @@ def inspect(workspace, output_file, measurement): click.echo() if output_file: - with open(output_file, 'w+') as out_file: + with open(output_file, "w+", encoding="utf-8") as out_file: json.dump(result, out_file, indent=4, sort_keys=True) log.debug(f"Written to {output_file:s}") @@ -189,7 +189,7 @@ def prune( See :func:`pyhf.workspace.Workspace.prune` for more information. """ - with click.open_file(workspace, 'r') as specstream: + with click.open_file(workspace, "r", encoding="utf-8") as specstream: spec = json.load(specstream) ws = Workspace(spec) @@ -204,7 +204,7 @@ def prune( if output_file is None: click.echo(json.dumps(pruned_ws, indent=4, sort_keys=True)) else: - with open(output_file, 'w+') as out_file: + with open(output_file, "w+", encoding="utf-8") as out_file: json.dump(pruned_ws, out_file, indent=4, sort_keys=True) log.debug(f"Written to {output_file:s}") @@ -253,7 +253,7 @@ def rename(workspace, output_file, channel, sample, modifier, measurement): See :func:`pyhf.workspace.Workspace.rename` for more information. """ - with click.open_file(workspace, 'r') as specstream: + with click.open_file(workspace, "r", encoding="utf-8") as specstream: spec = json.load(specstream) ws = Workspace(spec) @@ -267,7 +267,7 @@ def rename(workspace, output_file, channel, sample, modifier, measurement): if output_file is None: click.echo(json.dumps(renamed_ws, indent=4, sort_keys=True)) else: - with open(output_file, 'w+') as out_file: + with open(output_file, "w+", encoding="utf-8") as out_file: json.dump(renamed_ws, out_file, indent=4, sort_keys=True) log.debug(f"Written to {output_file:s}") @@ -298,10 +298,10 @@ def combine(workspace_one, workspace_two, join, output_file, merge_channels): See :func:`pyhf.workspace.Workspace.combine` for more information. """ - with click.open_file(workspace_one, 'r') as specstream: + with click.open_file(workspace_one, "r", encoding="utf-8") as specstream: spec_one = json.load(specstream) - with click.open_file(workspace_two, 'r') as specstream: + with click.open_file(workspace_two, "r", encoding="utf-8") as specstream: spec_two = json.load(specstream) ws_one = Workspace(spec_one) @@ -313,7 +313,7 @@ def combine(workspace_one, workspace_two, join, output_file, merge_channels): if output_file is None: click.echo(json.dumps(combined_ws, indent=4, sort_keys=True)) else: - with open(output_file, 'w+') as out_file: + with open(output_file, "w+", encoding="utf-8") as out_file: json.dump(combined_ws, out_file, indent=4, sort_keys=True) log.debug(f"Written to {output_file:s}") @@ -347,7 +347,7 @@ def digest(workspace, algorithm, output_json): $ curl -sL https://raw.githubusercontent.com/scikit-hep/pyhf/master/docs/examples/json/2-bin_1-channel.json | pyhf digest sha256:dad8822af55205d60152cbe4303929042dbd9d4839012e055e7c6b6459d68d73 """ - with click.open_file(workspace, 'r') as specstream: + with click.open_file(workspace, "r", encoding="utf-8") as specstream: spec = json.load(specstream) workspace = Workspace(spec) @@ -393,7 +393,7 @@ def sort(workspace, output_file): """ - with click.open_file(workspace, 'r') as specstream: + with click.open_file(workspace, "r", encoding="utf-8") as specstream: spec = json.load(specstream) workspace = Workspace(spec) @@ -402,6 +402,6 @@ def sort(workspace, output_file): if output_file is None: click.echo(json.dumps(sorted_ws, indent=4, sort_keys=True)) else: - with open(output_file, 'w+') as out_file: + with open(output_file, "w+", encoding="utf-8") as out_file: json.dump(sorted_ws, out_file, indent=4, sort_keys=True) log.debug(f"Written to {output_file}") diff --git a/src/pyhf/contrib/utils.py b/src/pyhf/contrib/utils.py index cca215ece3..da6daedd32 100644 --- a/src/pyhf/contrib/utils.py +++ b/src/pyhf/contrib/utils.py @@ -6,7 +6,7 @@ from io import BytesIO from pathlib import Path from shutil import rmtree -from urllib.parse import urlparse +from urllib.parse import urlsplit from pyhf import exceptions @@ -50,7 +50,7 @@ def download(archive_url, output_directory, force=False, compress=False): """ if not force: valid_hosts = ["www.hepdata.net", "doi.org"] - netloc = urlparse(archive_url).netloc + netloc = urlsplit(archive_url).netloc if netloc not in valid_hosts: raise exceptions.InvalidArchiveHost( f"{netloc} is not an approved archive host: {', '.join(str(host) for host in valid_hosts)}\n" diff --git a/src/pyhf/data/citation.bib b/src/pyhf/data/citation.bib index cb5c5f2749..e1cfb34843 100644 --- a/src/pyhf/data/citation.bib +++ b/src/pyhf/data/citation.bib @@ -1,10 +1,10 @@ @software{pyhf, author = {Lukas Heinrich and Matthew Feickert and Giordon Stark}, - title = "{pyhf: v0.7.0rc3}", - version = {0.7.0rc3}, + title = "{pyhf: v0.7.0rc4}", + version = {0.7.0rc4}, doi = {10.5281/zenodo.1169739}, url = {https://doi.org/10.5281/zenodo.1169739}, - note = {https://github.com/scikit-hep/pyhf/releases/tag/v0.7.0rc3} + note = {https://github.com/scikit-hep/pyhf/releases/tag/v0.7.0rc4} } @article{pyhf_joss, diff --git a/src/pyhf/exceptions/__init__.py b/src/pyhf/exceptions/__init__.py index 1cbbb4b83c..6d7e55f12f 100644 --- a/src/pyhf/exceptions/__init__.py +++ b/src/pyhf/exceptions/__init__.py @@ -1,4 +1,5 @@ import sys +from warnings import warn __all__ = [ "FailedMinimization", @@ -175,3 +176,15 @@ def __init__(self, result): result, 'message', "Unknown failure. See fit result for more details." ) super().__init__(message) + + +# Deprecated APIs +def _deprecated_api_warning( + deprecated_api, new_api, deprecated_release, remove_release +): + warn( + f"{deprecated_api} is deprecated in favor of {new_api} as of pyhf v{deprecated_release} and will be removed in pyhf v{remove_release}." + + f" Please use {new_api}.", + DeprecationWarning, + stacklevel=3, # Raise to user level + ) diff --git a/src/pyhf/infer/intervals/__init__.py b/src/pyhf/infer/intervals/__init__.py new file mode 100644 index 0000000000..0f2f928cdd --- /dev/null +++ b/src/pyhf/infer/intervals/__init__.py @@ -0,0 +1,30 @@ +"""Interval estimation""" +import pyhf.infer.intervals.upper_limits + +__all__ = ["upper_limits.upper_limit"] + + +def __dir__(): + return __all__ + + +def upperlimit( + data, model, scan=None, level=0.05, return_results=False, **hypotest_kwargs +): + """ + .. deprecated:: 0.7.0 + Use :func:`~pyhf.infer.intervals.upper_limits.upper_limit` instead. + .. warning:: :func:`~pyhf.infer.intervals.upperlimit` will be removed in + ``pyhf`` ``v0.9.0``. + """ + from pyhf.exceptions import _deprecated_api_warning + + _deprecated_api_warning( + "pyhf.infer.intervals.upperlimit", + "pyhf.infer.intervals.upper_limits.upper_limit", + "0.7.0", + "0.9.0", + ) + return pyhf.infer.intervals.upper_limits.upper_limit( + data, model, scan, level, return_results, **hypotest_kwargs + ) diff --git a/src/pyhf/infer/intervals.py b/src/pyhf/infer/intervals/upper_limits.py similarity index 57% rename from src/pyhf/infer/intervals.py rename to src/pyhf/infer/intervals/upper_limits.py index 4ffba62eb8..12220909a8 100644 --- a/src/pyhf/infer/intervals.py +++ b/src/pyhf/infer/intervals/upper_limits.py @@ -1,13 +1,11 @@ """Interval estimation""" -from warnings import warn - import numpy as np from scipy.optimize import toms748 from pyhf import get_backend from pyhf.infer import hypotest -__all__ = ["upperlimit"] +__all__ = ["upper_limit", "linear_grid_scan", "toms748_scan"] def __dir__(): @@ -19,17 +17,16 @@ def _interp(x, xp, fp): return tb.astensor(np.interp(x, xp.tolist(), fp.tolist())) -def upperlimit_auto( +def toms748_scan( data, model, - low, - high, + bounds_low, + bounds_up, level=0.05, atol=2e-12, - rtol=None, - calctype='asymptotics', - test_stat='qtilde', - from_upperlimit_fn=False, + rtol=1e-4, + from_upper_limit_fn=False, + **hypotest_kwargs, ): """ Calculate an upper limit interval ``(0, poi_up)`` for a single @@ -40,79 +37,74 @@ def upperlimit_auto( >>> import numpy as np >>> import pyhf >>> pyhf.set_backend("numpy") - >>> model = pyhf.simplemodels.hepdata_like( - ... signal_data=[12.0, 11.0], bkg_data=[50.0, 52.0], bkg_uncerts=[3.0, 7.0] + >>> model = pyhf.simplemodels.uncorrelated_background( + ... signal=[12.0, 11.0], bkg=[50.0, 52.0], bkg_uncertainty=[3.0, 7.0] ... ) >>> observations = [51, 48] >>> data = pyhf.tensorlib.astensor(observations + model.config.auxdata) - >>> obs_limit, exp_limits = pyhf.infer.intervals.upperlimit_auto( - ... data, model, 0., 5. + >>> obs_limit, exp_limits = pyhf.infer.intervals.upper_limits.toms748_scan( + ... data, model, 0., 5., rtol=0.01 ... ) >>> obs_limit array(1.01156939) >>> exp_limits - [array(0.55988001), array(0.75702336), array(1.06234693), array(1.50116923), array(2.05078596)] + [array(0.5600747), array(0.75702605), array(1.06234693), array(1.50116923), array(2.05078912)] Args: data (:obj:`tensor`): The observed data. model (~pyhf.pdf.Model): The statistical model adhering to the schema ``model.json``. - low (:obj:`float`): Lower boundary of search region - high (:obj:`float`): Higher boundary of search region + bounds_low (:obj:`float`): Lower boundary of search interval. + bounds_up (:obj:`float`): Upper boundary of search interval. level (:obj:`float`): The threshold value to evaluate the interpolated results at. - Defaults to 0.05. - atol (:obj:`float`): Absolute tolerance. Defaults to 1e-12. The iteration will end when the - result is within absolute *or* relative tolerance of the true limit. - rtol (:obj:`float`): Relative tolerance. For optimal performance this argument should be set - to the highest acceptable relative tolerance, though it will default - to 1e-15 if not set. - calctype (:obj:`str`): Calculator to use for hypothesis tests. Choose 'asymptotics' (default) - or 'toybased'. - test_stat (:obj:`str`): Test statistic to use. Choose 'qtilde' (default), 'q', or 'q0'. + Defaults to ``0.05``. + atol (:obj:`float`): Absolute tolerance. + Defaults to ``1e-12``. + The iteration will end when the result is within absolute + *or* relative tolerance of the true limit. + rtol (:obj:`float`): Relative tolerance. + For optimal performance this argument should be set + to the highest acceptable relative tolerance. + hypotest_kwargs (:obj:`string`): Kwargs for the calls to + :class:`~pyhf.infer.hypotest` to configure the fits. Returns: Tuple of Tensors: - Tensor: The observed upper limit on the POI. - Tensor: The expected upper limits on the POI. - """ - if rtol is None: - warn( - "upperlimit_auto: rtol not provided, defaulting to 1e-15. " - "For optimal performance rtol should be set to the highest acceptable relative tolerance." - ) - rtol = 1e-15 + .. versionadded:: 0.7.0 + """ cache = {} - def f_all(mu): - if mu in cache: - return cache[mu] - cache[mu] = hypotest( - mu, - data, - model, - test_stat=test_stat, - calctype=calctype, - return_expected_set=True, - ) - return cache[mu] - - def f(mu, limit=0): + def f_cached(poi): + if poi not in cache: + cache[poi] = hypotest( + poi, + data, + model, + return_expected_set=True, + **hypotest_kwargs, + ) + return cache[poi] + + def f(poi, level, limit=0): # Use integers for limit so we don't need a string comparison - if limit == 0: - # Obs - return f_all(mu)[0] - level - # Exp - # (These are in the order -2, -1, 0, 1, 2 sigma) - return f_all(mu)[1][limit - 1] - level + # limit == 0: Observed + # else: expected + return ( + f_cached(poi)[0] - level + if limit == 0 + else f_cached(poi)[1][limit - 1] - level + ) def best_bracket(limit): # return best bracket - ks = np.array(list(cache.keys())) - vals = np.array( + ks = np.asarray(list(cache.keys())) + vals = np.asarray( [ - v[0] - level if limit == 0 else v[1][limit - 1] - level - for v in cache.values() + value[0] - level if limit == 0 else value[1][limit - 1] - level + for value in cache.values() ] ) pos = vals >= 0 @@ -121,33 +113,40 @@ def best_bracket(limit): upper = ks[neg][np.argmax(vals[neg])] return (lower, upper) - # extend low and high if they don't bracket CLs level - low_res = f_all(low) - while np.any(np.array(low_res[0] + low_res[1]) < 0.05): - low /= 2 - low_res = f_all(low) - high_res = f_all(high) - while np.any(np.array(high_res[0] + high_res[1]) > 0.05): - high *= 2 - high_res = f_all(high) + # extend bounds_low and bounds_up if they don't bracket CLs level + lower_results = f_cached(bounds_low) + # {lower,upper}_results[0] is an array and {lower,upper}_results[1] is a + # list of arrays so need to turn {lower,upper}_results[0] into list to + # concatenate them + while np.any(np.asarray([lower_results[0]] + lower_results[1]) < level): + bounds_low /= 2 + lower_results = f_cached(bounds_low) + upper_results = f_cached(bounds_up) + while np.any(np.asarray([upper_results[0]] + upper_results[1]) > level): + bounds_up *= 2 + upper_results = f_cached(bounds_up) tb, _ = get_backend() - obs = tb.astensor(toms748(f, low, high, args=(0), k=2, xtol=atol, rtol=rtol)) + obs = tb.astensor( + toms748(f, bounds_low, bounds_up, args=(level, 0), k=2, xtol=atol, rtol=rtol) + ) exp = [ - tb.astensor(toms748(f, *best_bracket(i), args=(i), k=2, xtol=atol, rtol=rtol)) - for i in range(1, 6) + tb.astensor( + toms748(f, *best_bracket(idx), args=(level, idx), k=2, xtol=atol, rtol=rtol) + ) + for idx in range(1, 6) ] - if from_upperlimit_fn: + if from_upper_limit_fn: return obs, exp, (list(cache.keys()), list(cache.values())) return obs, exp -def upperlimit_fixedscan( +def linear_grid_scan( data, model, scan, level=0.05, return_results=False, **hypotest_kwargs ): """ Calculate an upper limit interval ``(0, poi_up)`` for a single - Parameter of Interest (POI) using a fixed scan through POI-space. + Parameter of Interest (POI) using a linear scan through POI-space. Example: >>> import numpy as np @@ -159,7 +158,7 @@ def upperlimit_fixedscan( >>> observations = [51, 48] >>> data = pyhf.tensorlib.astensor(observations + model.config.auxdata) >>> scan = np.linspace(0, 5, 21) - >>> obs_limit, exp_limits, (scan, results) = pyhf.infer.intervals.upperlimit( + >>> obs_limit, exp_limits, (scan, results) = pyhf.infer.intervals.upper_limits.upper_limit( ... data, model, scan, return_results=True ... ) >>> obs_limit @@ -184,6 +183,8 @@ def upperlimit_fixedscan( - Tuple of Tensors: The given ``scan`` along with the :class:`~pyhf.infer.hypotest` results at each test POI. Only returned when ``return_results`` is ``True``. + + .. versionadded:: 0.7.0 """ tb, _ = get_backend() results = [ @@ -193,10 +194,10 @@ def upperlimit_fixedscan( obs = tb.astensor([[r[0]] for r in results]) exp = tb.astensor([[r[1][idx] for idx in range(5)] for r in results]) - result_arrary = tb.concatenate([obs, exp], axis=1).T + result_array = tb.concatenate([obs, exp], axis=1).T # observed limit and the (0, +-1, +-2)sigma expected limits - limits = [_interp(level, result_arrary[idx][::-1], scan[::-1]) for idx in range(6)] + limits = [_interp(level, result_array[idx][::-1], scan[::-1]) for idx in range(6)] obs_limit, exp_limits = limits[0], limits[1:] if return_results: @@ -204,10 +205,12 @@ def upperlimit_fixedscan( return obs_limit, exp_limits -def upperlimit(data, model, scan, level=0.05, return_results=False): +def upper_limit( + data, model, scan=None, level=0.05, return_results=False, **hypotest_kwargs +): """ - Calculate an upper limit interval ``(0, poi_up)`` for a single - Parameter of Interest (POI) using root-finding or a fixed scan through POI-space. + Calculate an upper limit interval ``(0, poi_up)`` for a single Parameter of + Interest (POI) using root-finding or a linear scan through POI-space. Example: >>> import numpy as np @@ -219,7 +222,7 @@ def upperlimit(data, model, scan, level=0.05, return_results=False): >>> observations = [51, 48] >>> data = pyhf.tensorlib.astensor(observations + model.config.auxdata) >>> scan = np.linspace(0, 5, 21) - >>> obs_limit, exp_limits, (scan, results) = pyhf.infer.intervals.upperlimit( + >>> obs_limit, exp_limits, (scan, results) = pyhf.infer.intervals.upper_limits.upper_limit( ... data, model, scan, return_results=True ... ) >>> obs_limit @@ -230,7 +233,8 @@ def upperlimit(data, model, scan, level=0.05, return_results=False): Args: data (:obj:`tensor`): The observed data. model (~pyhf.pdf.Model): The statistical model adhering to the schema ``model.json``. - scan (:obj:`iterable` or "auto"): Iterable of POI values or "auto" to use ``upperlimit_auto``. + scan (:obj:`iterable` or ``None``): Iterable of POI values or ``None`` to use + :class:`~pyhf.infer.intervals.upper_limits.toms748_scan`. level (:obj:`float`): The threshold value to evaluate the interpolated results at. return_results (:obj:`bool`): Whether to return the per-point results. @@ -242,15 +246,25 @@ def upperlimit(data, model, scan, level=0.05, return_results=False): - Tuple of Tensors: The given ``scan`` along with the :class:`~pyhf.infer.hypotest` results at each test POI. Only returned when ``return_results`` is ``True``. + + .. versionadded:: 0.7.0 """ - if isinstance(scan, str) and scan.lower() == 'auto': - bounds = model.config.suggested_bounds()[ - model.config.par_slice(model.config.poi_name).start - ] - obs_limit, exp_limit, results = upperlimit_auto( - data, model, bounds[0], bounds[1], rtol=1e-3, from_upperlimit_fn=True + if scan is not None: + return linear_grid_scan( + data, model, scan, level, return_results, **hypotest_kwargs ) - if return_results: - return obs_limit, exp_limit, results - return obs_limit, exp_limit - return upperlimit_fixedscan(data, model, scan, level, return_results) + # else: + bounds = model.config.suggested_bounds()[ + model.config.par_slice(model.config.poi_name).start + ] + obs_limit, exp_limit, results = toms748_scan( + data, + model, + bounds[0], + bounds[1], + from_upper_limit_fn=True, + **hypotest_kwargs, + ) + if return_results: + return obs_limit, exp_limit, results + return obs_limit, exp_limit diff --git a/src/pyhf/schema/loader.py b/src/pyhf/schema/loader.py index c046bec409..920766c4dc 100644 --- a/src/pyhf/schema/loader.py +++ b/src/pyhf/schema/loader.py @@ -50,7 +50,7 @@ def load_schema(schema_id: str): raise pyhf.exceptions.SchemaNotFound( f'The schema {schema_id} was not found. Do you have the right version or the right path? {path}' ) - with path.open() as json_schema: + with path.open(encoding="utf-8") as json_schema: schema = json.load(json_schema) variables.SCHEMA_CACHE[schema['$id']] = schema return variables.SCHEMA_CACHE[schema['$id']] diff --git a/src/pyhf/utils.py b/src/pyhf/utils.py index 23f374515d..c4a855430e 100644 --- a/src/pyhf/utils.py +++ b/src/pyhf/utils.py @@ -111,7 +111,7 @@ def citation(oneline=False): >>> import pyhf >>> pyhf.utils.citation(oneline=True) - '@software{pyhf, author = {Lukas Heinrich and Matthew Feickert and Giordon Stark}, title = "{pyhf: v0.7.0rc3}", version = {0.7.0rc3}, doi = {10.5281/zenodo.1169739}, url = {https://doi.org/10.5281/zenodo.1169739}, note = {https://github.com/scikit-hep/pyhf/releases/tag/v0.7.0rc3}}@article{pyhf_joss, doi = {10.21105/joss.02823}, url = {https://doi.org/10.21105/joss.02823}, year = {2021}, publisher = {The Open Journal}, volume = {6}, number = {58}, pages = {2823}, author = {Lukas Heinrich and Matthew Feickert and Giordon Stark and Kyle Cranmer}, title = {pyhf: pure-Python implementation of HistFactory statistical models}, journal = {Journal of Open Source Software}}' + '@software{pyhf, author = {Lukas Heinrich and Matthew Feickert and Giordon Stark}, title = "{pyhf: v0.7.0rc4}", version = {0.7.0rc4}, doi = {10.5281/zenodo.1169739}, url = {https://doi.org/10.5281/zenodo.1169739}, note = {https://github.com/scikit-hep/pyhf/releases/tag/v0.7.0rc4}}@article{pyhf_joss, doi = {10.21105/joss.02823}, url = {https://doi.org/10.21105/joss.02823}, year = {2021}, publisher = {The Open Journal}, volume = {6}, number = {58}, pages = {2823}, author = {Lukas Heinrich and Matthew Feickert and Giordon Stark and Kyle Cranmer}, title = {pyhf: pure-Python implementation of HistFactory statistical models}, journal = {Journal of Open Source Software}}' Keyword Args: oneline (:obj:`bool`): Whether to provide citation with new lines (default) or as a one-liner. diff --git a/src/pyhf/writexml.py b/src/pyhf/writexml.py index 0567c94031..8d3ecd3ca3 100644 --- a/src/pyhf/writexml.py +++ b/src/pyhf/writexml.py @@ -289,7 +289,7 @@ def writexml(spec, specdir, data_rootdir, resultprefix): channelfilename = str( Path(specdir).joinpath(f'{resultprefix}_{channelspec["name"]}.xml') ) - with open(channelfilename, 'w') as channelfile: + with open(channelfilename, "w", encoding="utf-8") as channelfile: channel = build_channel(spec, channelspec, spec.get('observations')) indent(channel) channelfile.write( diff --git a/tbump.toml b/tbump.toml index 60204abbc2..50f4ff2fb7 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/scikit-hep/pyhf/" [version] -current = "0.7.0rc3" +current = "0.7.0rc4" # Example of a semver regexp. # Make sure this matches current_version before @@ -19,7 +19,7 @@ regex = ''' [git] # The current version will get updated when tbump is run -message_template = "Bump version: 0.7.0rc3 → {new_version}" +message_template = "Bump version: 0.7.0rc4 → {new_version}" tag_template = "v{new_version}" # For each file to patch, add a [[file]] config diff --git a/tests/conftest.py b/tests/conftest.py index 96c1d11918..673d30d3b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,7 @@ @pytest.fixture def get_json_from_tarfile(): def _get_json_from_tarfile(archive_data_path, json_name): - with tarfile.open(archive_data_path, "r:gz") as archive: + with tarfile.open(archive_data_path, "r:gz", encoding="utf-8") as archive: json_file = ( archive.extractfile(archive.getmember(json_name)).read().decode("utf8") ) diff --git a/tests/constraints.txt b/tests/constraints.txt index 5f695f17fc..4e2e4d860c 100644 --- a/tests/constraints.txt +++ b/tests/constraints.txt @@ -1,5 +1,5 @@ # core -scipy==1.1.0 +scipy==1.2.0 # c.f. PR #1274 click==8.0.0 # c.f. PR #1958, #1909 tqdm==4.56.0 jsonschema==4.15.0 # c.f. PR #1979 diff --git a/tests/contrib/baseline/test_plot_results_no_axis.png b/tests/contrib/baseline/test_plot_results_no_axis.png index 62d432a5a5..f52ca48883 100644 Binary files a/tests/contrib/baseline/test_plot_results_no_axis.png and b/tests/contrib/baseline/test_plot_results_no_axis.png differ diff --git a/tests/contrib/test_contrib_utils.py b/tests/contrib/test_contrib_utils.py index 075093966d..5a0b69261b 100644 --- a/tests/contrib/test_contrib_utils.py +++ b/tests/contrib/test_contrib_utils.py @@ -11,25 +11,35 @@ @pytest.fixture(scope="function") def tarfile_path(tmpdir): - with open(tmpdir.join("test_file.txt").strpath, "w") as write_file: + with open( + tmpdir.join("test_file.txt").strpath, "w", encoding="utf-8" + ) as write_file: write_file.write("test file") - with tarfile.open(tmpdir.join("test_tar.tar.gz").strpath, mode="w:gz") as archive: + with tarfile.open( + tmpdir.join("test_tar.tar.gz").strpath, mode="w:gz", encoding="utf-8" + ) as archive: archive.add(tmpdir.join("test_file.txt").strpath) return Path(tmpdir.join("test_tar.tar.gz").strpath) @pytest.fixture(scope="function") def tarfile_uncompressed_path(tmpdir): - with open(tmpdir.join("test_file.txt").strpath, "w") as write_file: + with open( + tmpdir.join("test_file.txt").strpath, "w", encoding="utf-8" + ) as write_file: write_file.write("test file") - with tarfile.open(tmpdir.join("test_tar.tar").strpath, mode="w") as archive: + with tarfile.open( + tmpdir.join("test_tar.tar").strpath, mode="w", encoding="utf-8" + ) as archive: archive.add(tmpdir.join("test_file.txt").strpath) return Path(tmpdir.join("test_tar.tar").strpath) @pytest.fixture(scope="function") def zipfile_path(tmpdir): - with open(tmpdir.join("test_file.txt").strpath, "w") as write_file: + with open( + tmpdir.join("test_file.txt").strpath, "w", encoding="utf-8" + ) as write_file: write_file.write("test file") with zipfile.ZipFile(tmpdir.join("test_zip.zip").strpath, "w") as archive: archive.write(tmpdir.join("test_file.txt").strpath) diff --git a/tests/contrib/test_viz.py b/tests/contrib/test_viz.py index 5c04ad99b6..7a49eddb49 100644 --- a/tests/contrib/test_viz.py +++ b/tests/contrib/test_viz.py @@ -1,4 +1,5 @@ import json +import sys import matplotlib import matplotlib.pyplot as plt @@ -13,7 +14,7 @@ def test_brazil_band_collection(datadir): - data = json.load(datadir.joinpath("hypotest_results.json").open()) + data = json.load(datadir.joinpath("hypotest_results.json").open(encoding="utf-8")) fig = Figure() ax = fig.subplots() @@ -31,7 +32,9 @@ def test_brazil_band_collection(datadir): assert brazil_band_collection.clb is None assert brazil_band_collection.axes == ax - data = json.load(datadir.joinpath("tail_probs_hypotest_results.json").open()) + data = json.load( + datadir.joinpath("tail_probs_hypotest_results.json").open(encoding="utf-8") + ) fig = Figure() ax = fig.subplots() @@ -52,7 +55,7 @@ def test_brazil_band_collection(datadir): @pytest.mark.mpl_image_compare def test_plot_results(datadir): - data = json.load(datadir.joinpath("hypotest_results.json").open()) + data = json.load(datadir.joinpath("hypotest_results.json").open(encoding="utf-8")) fig = Figure() ax = fig.subplots() @@ -65,8 +68,12 @@ def test_plot_results(datadir): @pytest.mark.mpl_image_compare +@pytest.mark.xfail( + sys.version_info < (3, 8), + reason="baseline image generated with matplotlib v3.6.0 which is Python 3.8+", +) def test_plot_results_no_axis(datadir): - data = json.load(datadir.joinpath("hypotest_results.json").open()) + data = json.load(datadir.joinpath("hypotest_results.json").open(encoding="utf-8")) matplotlib.use("agg") # Use non-gui backend fig, ax = plt.subplots() @@ -78,7 +85,9 @@ def test_plot_results_no_axis(datadir): @pytest.mark.mpl_image_compare def test_plot_results_components(datadir): - data = json.load(datadir.joinpath("tail_probs_hypotest_results.json").open()) + data = json.load( + datadir.joinpath("tail_probs_hypotest_results.json").open(encoding="utf-8") + ) fig = Figure() ax = fig.subplots() @@ -90,7 +99,9 @@ def test_plot_results_components(datadir): @pytest.mark.mpl_image_compare def test_plot_results_components_no_clb(datadir): - data = json.load(datadir.joinpath("tail_probs_hypotest_results.json").open()) + data = json.load( + datadir.joinpath("tail_probs_hypotest_results.json").open(encoding="utf-8") + ) fig = Figure() ax = fig.subplots() @@ -110,7 +121,9 @@ def test_plot_results_components_no_clb(datadir): @pytest.mark.mpl_image_compare def test_plot_results_components_no_clsb(datadir): - data = json.load(datadir.joinpath("tail_probs_hypotest_results.json").open()) + data = json.load( + datadir.joinpath("tail_probs_hypotest_results.json").open(encoding="utf-8") + ) fig = Figure() ax = fig.subplots() @@ -130,7 +143,9 @@ def test_plot_results_components_no_clsb(datadir): @pytest.mark.mpl_image_compare def test_plot_results_components_no_cls(datadir): - data = json.load(datadir.joinpath("tail_probs_hypotest_results.json").open()) + data = json.load( + datadir.joinpath("tail_probs_hypotest_results.json").open(encoding="utf-8") + ) fig = Figure() ax = fig.subplots() @@ -158,7 +173,7 @@ def test_plot_results_components_data_structure(datadir): """ test results should have format of: [CLs_obs, [CLsb, CLb], [CLs_exp band]] """ - data = json.load(datadir.joinpath("hypotest_results.json").open()) + data = json.load(datadir.joinpath("hypotest_results.json").open(encoding="utf-8")) fig = Figure() ax = fig.subplots() diff --git a/tests/test_export.py b/tests/test_export.py index 4700ff7988..bba0aa224e 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -53,7 +53,10 @@ def spec_staterror(): def spec_histosys(): - source = json.load(open('validation/data/2bin_histosys_example2.json')) + with open( + "validation/data/2bin_histosys_example2.json", encoding="utf-8" + ) as spec_file: + source = json.load(spec_file) spec = { 'channels': [ { @@ -88,7 +91,10 @@ def spec_histosys(): def spec_normsys(): - source = json.load(open('validation/data/2bin_histosys_example2.json')) + with open( + "validation/data/2bin_histosys_example2.json", encoding="utf-8" + ) as spec_file: + source = json.load(spec_file) spec = { 'channels': [ { @@ -120,7 +126,10 @@ def spec_normsys(): def spec_shapesys(): - source = json.load(open('validation/data/2bin_histosys_example2.json')) + with open( + "validation/data/2bin_histosys_example2.json", encoding="utf-8" + ) as spec_file: + source = json.load(spec_file) spec = { 'channels': [ { @@ -148,7 +157,10 @@ def spec_shapesys(): def spec_shapefactor(): - source = json.load(open('validation/data/2bin_histosys_example2.json')) + with open( + "validation/data/2bin_histosys_example2.json", encoding="utf-8" + ) as spec_file: + source = json.load(spec_file) spec = { 'channels': [ { @@ -408,7 +420,9 @@ def test_export_root_histogram(mocker, tmp_path): with uproot.recreate(tmp_path.joinpath("test_export_root_histogram.root")) as file: file["hist"] = pyhf.writexml._ROOT_DATA_FILE["hist"] - with uproot.open(tmp_path.joinpath("test_export_root_histogram.root")) as file: + with uproot.open( + tmp_path.joinpath("test_export_root_histogram.root"), encoding="utf-8" + ) as file: assert file["hist"].values().tolist() == [0, 1, 2, 3, 4, 5, 6, 7, 8] assert file["hist"].axis().edges().tolist() == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] assert file["hist"].name == "hist" @@ -425,7 +439,9 @@ def test_integer_data(datadir, mocker): """ Test that a spec with only integer data will be written correctly """ - with open(datadir.joinpath("workspace_integer_data.json")) as spec_file: + with open( + datadir.joinpath("workspace_integer_data.json"), encoding="utf-8" + ) as spec_file: spec = json.load(spec_file) channel_spec = spec["channels"][0] mocker.patch("pyhf.writexml._ROOT_DATA_FILE") @@ -443,7 +459,7 @@ def test_integer_data(datadir, mocker): ids=['no_inits', 'no_bounds'], ) def test_issue1814(datadir, mocker, fname, val, low, high): - with open(datadir / fname) as spec_file: + with open(datadir / fname, encoding="utf-8") as spec_file: spec = json.load(spec_file) modifierspec = {'data': None, 'name': 'mu_sig', 'type': 'normfactor'} diff --git a/tests/test_infer.py b/tests/test_infer.py index 0321e31b13..a3e9094140 100644 --- a/tests/test_infer.py +++ b/tests/test_infer.py @@ -1,10 +1,13 @@ -import pytest -import pyhf +import warnings + import numpy as np +import pytest import scipy.stats +import pyhf + -@pytest.fixture(scope='module') +@pytest.fixture(scope="function") def hypotest_args(): pdf = pyhf.simplemodels.uncorrelated_background( signal=[12.0, 11.0], bkg=[50.0, 52.0], bkg_uncertainty=[3.0, 7.0] @@ -20,12 +23,14 @@ def check_uniform_type(in_list): ) -def test_upperlimit_auto(tmpdir, hypotest_args): +def test_toms748_scan(tmpdir, hypotest_args): """ - Check that the upper limit autoscan returns the correct structure and values + Test the upper limit toms748 scan returns the correct structure and values """ _, data, model = hypotest_args - results = pyhf.infer.intervals.upperlimit_auto(data, model, 0, 5, rtol=1e-8) + results = pyhf.infer.intervals.upper_limits.toms748_scan( + data, model, 0, 5, rtol=1e-8 + ) assert len(results) == 2 observed_limit, expected_limits = results observed_cls = pyhf.infer.hypotest( @@ -52,30 +57,57 @@ def test_upperlimit_auto(tmpdir, hypotest_args): assert expected_cls == pytest.approx(0.05) -def test_upperlimit_against_auto(tmpdir, hypotest_args): +def test_toms748_scan_bounds_extension(hypotest_args): """ - Check that upperlimit and upperlimit_auto return similar results + Test the upper limit toms748 scan bounds can correctly extend to bracket the CLs level """ _, data, model = hypotest_args - results_auto = pyhf.infer.intervals.upperlimit_auto(data, model, 0, 5, rtol=1e-3) + results_default = pyhf.infer.intervals.upper_limits.toms748_scan( + data, model, 0, 5, rtol=1e-8 + ) + observed_limit_default, expected_limits_default = results_default + + # Force bounds_low to expand + observed_limit, expected_limits = pyhf.infer.intervals.upper_limits.toms748_scan( + data, model, 3, 5, rtol=1e-8 + ) + + assert observed_limit == pytest.approx(observed_limit_default) + assert np.allclose(np.asarray(expected_limits), np.asarray(expected_limits_default)) + + # Force bounds_up to expand + observed_limit, expected_limits = pyhf.infer.intervals.upper_limits.toms748_scan( + data, model, 0, 1, rtol=1e-8 + ) + assert observed_limit == pytest.approx(observed_limit_default) + assert np.allclose(np.asarray(expected_limits), np.asarray(expected_limits_default)) + + +def test_upper_limit_against_auto(hypotest_args): + """ + Test upper_limit linear scan and toms748_scan return similar results + """ + _, data, model = hypotest_args + results_auto = pyhf.infer.intervals.upper_limits.toms748_scan( + data, model, 0, 5, rtol=1e-3 + ) obs_auto, exp_auto = results_auto - results_linear = pyhf.infer.intervals.upperlimit( + results_linear = pyhf.infer.intervals.upper_limits.upper_limit( data, model, scan=np.linspace(0, 5, 21) ) obs_linear, exp_linear = results_linear # Can't expect these to be much closer given the low granularity of the linear scan - assert obs_auto == pytest.approx(obs_linear, abs=0.25) - # For some reason, this isn't working with the full list at once - for i in range(5): - assert exp_auto[i] == pytest.approx(exp_linear[i], abs=0.25) + assert obs_auto == pytest.approx(obs_linear, abs=0.1) + assert np.allclose(exp_auto, exp_linear, atol=0.1) -def test_upperlimit(tmpdir, hypotest_args): +def test_upper_limit(hypotest_args): """ Check that the default return structure of pyhf.infer.hypotest is as expected """ _, data, model = hypotest_args - results = pyhf.infer.intervals.upperlimit(data, model, scan=np.linspace(0, 5, 11)) + scan = np.linspace(0, 5, 11) + results = pyhf.infer.intervals.upper_limits.upper_limit(data, model, scan=scan) assert len(results) == 2 observed_limit, expected_limits = results assert observed_limit == pytest.approx(1.0262704738584554) @@ -83,14 +115,23 @@ def test_upperlimit(tmpdir, hypotest_args): [0.65765653, 0.87999725, 1.12453992, 1.50243428, 2.09232927] ) + results = pyhf.infer.intervals.upper_limits.upper_limit(data, model) + assert len(results) == 2 + observed_limit, expected_limits = results + assert observed_limit == pytest.approx(1.01156939) + assert expected_limits == pytest.approx( + [0.55988001, 0.75702336, 1.06234693, 1.50116923, 2.05078596] + ) + -def test_upperlimit_with_kwargs(tmpdir, hypotest_args): +def test_upper_limit_with_kwargs(tmpdir, hypotest_args): """ Check that the default return structure of pyhf.infer.hypotest is as expected """ _, data, model = hypotest_args - results = pyhf.infer.intervals.upperlimit( - data, model, scan=np.linspace(0, 5, 11), test_stat="qtilde" + scan = np.linspace(0, 5, 11) + results = pyhf.infer.intervals.upper_limits.upper_limit( + data, model, scan=scan, test_stat="qtilde" ) assert len(results) == 2 observed_limit, expected_limits = results @@ -99,6 +140,30 @@ def test_upperlimit_with_kwargs(tmpdir, hypotest_args): [0.65765653, 0.87999725, 1.12453992, 1.50243428, 2.09232927] ) + # linear_grid_scan + results = pyhf.infer.intervals.upper_limits.upper_limit( + data, model, scan=scan, return_results=True + ) + assert len(results) == 3 + observed_limit, expected_limits, (_scan, point_results) = results + assert observed_limit == pytest.approx(1.0262704738584554) + assert expected_limits == pytest.approx( + [0.65765653, 0.87999725, 1.12453992, 1.50243428, 2.09232927] + ) + assert _scan.tolist() == scan.tolist() + assert len(_scan) == len(point_results) + + # toms748_scan + results = pyhf.infer.intervals.upper_limits.upper_limit( + data, model, return_results=True, rtol=1e-5 + ) + assert len(results) == 3 + observed_limit, expected_limits, (_scan, point_results) = results + assert observed_limit == pytest.approx(1.01156939) + assert expected_limits == pytest.approx( + [0.55988001, 0.75702336, 1.06234693, 1.50116923, 2.05078596] + ) + def test_mle_fit_default(tmpdir, hypotest_args): """ @@ -516,16 +581,16 @@ def test_toy_calculator(tmpdir, hypotest_args): ) -def test_fixed_poi(tmpdir, hypotest_args): +def test_fixed_poi(hypotest_args): """ Check that the return structure of pyhf.infer.hypotest with the addition of the return_expected keyword arg is as expected """ - _, _, pdf = hypotest_args - pdf.config.param_set('mu').suggested_fixed = [True] + test_poi, data, model = hypotest_args + model.config.param_set("mu").suggested_fixed = [True] with pytest.raises(pyhf.exceptions.InvalidModel): - pyhf.infer.hypotest(*hypotest_args) + pyhf.infer.hypotest(test_poi, data, model) def test_teststat_nan_guard(): @@ -560,3 +625,19 @@ def test_teststat_nan_guard(): test_poi, data, model, test_stat="qtilde", return_expected=True ) assert all(~np.isnan(result) for result in test_results) + + +# TODO: Remove after pyhf v0.9.0 is released +def test_deprecated_upperlimit(hypotest_args): + with warnings.catch_warnings(record=True) as _warning: + # Cause all warnings to always be triggered + warnings.simplefilter("always") + + _, data, model = hypotest_args + pyhf.infer.intervals.upperlimit(data, model, scan=np.linspace(0, 5, 11)) + assert len(_warning) == 1 + assert issubclass(_warning[-1].category, DeprecationWarning) + assert ( + "pyhf.infer.intervals.upperlimit is deprecated in favor of pyhf.infer.intervals.upper_limits.upper_limit" + in str(_warning[-1].message) + ) diff --git a/tests/test_modifiers.py b/tests/test_modifiers.py index 493326e2c8..6432e75e3b 100644 --- a/tests/test_modifiers.py +++ b/tests/test_modifiers.py @@ -184,11 +184,13 @@ def test_invalid_bin_wise_modifier(datadir, patch_file): Test that bin-wise modifiers will raise an exception if their data shape differs from their sample's. """ - spec = json.load(open(datadir.joinpath("spec.json"))) + with open(datadir.joinpath("spec.json"), encoding="utf-8") as spec_file: + spec = json.load(spec_file) assert pyhf.Model(spec) - patch = JsonPatch.from_string(open(datadir.joinpath(patch_file)).read()) + with open(datadir.joinpath(patch_file), encoding="utf-8") as spec_file: + patch = JsonPatch.from_string(spec_file.read()) bad_spec = patch.apply(spec) with pytest.raises(pyhf.exceptions.InvalidModifier): @@ -196,7 +198,9 @@ def test_invalid_bin_wise_modifier(datadir, patch_file): def test_issue1720_staterror_builder_mask(datadir): - with open(datadir.joinpath("issue1720_greedy_staterror.json")) as spec_file: + with open( + datadir.joinpath("issue1720_greedy_staterror.json"), encoding="utf-8" + ) as spec_file: spec = json.load(spec_file) spec["channels"][0]["samples"][1]["modifiers"][0]["type"] = "staterror" @@ -234,7 +238,9 @@ def test_issue1720_greedy_staterror(datadir, inits): """ Test that the staterror does not affect more samples than shapesys equivalently. """ - with open(datadir.joinpath("issue1720_greedy_staterror.json")) as spec_file: + with open( + datadir.joinpath("issue1720_greedy_staterror.json"), encoding="utf-8" + ) as spec_file: spec = json.load(spec_file) model_shapesys = pyhf.Workspace(spec).model() diff --git a/tests/test_patchset.py b/tests/test_patchset.py index b4e3d36724..64eb392350 100644 --- a/tests/test_patchset.py +++ b/tests/test_patchset.py @@ -12,7 +12,8 @@ ids=['patchset_good.json', 'patchset_good_2_patches.json'], ) def patchset(datadir, request): - spec = json.load(open(datadir.joinpath(request.param))) + with open(datadir.joinpath(request.param), encoding="utf-8") as spec_file: + spec = json.load(spec_file) return pyhf.PatchSet(spec) @@ -32,7 +33,8 @@ def patch(): ], ) def test_patchset_invalid_spec(datadir, patchset_file): - patchsetspec = json.load(open(datadir.joinpath(patchset_file))) + with open(datadir.joinpath(patchset_file), encoding="utf-8") as patch_file: + patchsetspec = json.load(patch_file) with pytest.raises(pyhf.exceptions.InvalidSpecification): pyhf.PatchSet(patchsetspec) @@ -46,7 +48,8 @@ def test_patchset_invalid_spec(datadir, patchset_file): ], ) def test_patchset_bad(datadir, patchset_file): - patchsetspec = json.load(open(datadir.joinpath(patchset_file))) + with open(datadir.joinpath(patchset_file), encoding="utf-8") as patch_file: + patchsetspec = json.load(patch_file) with pytest.raises(pyhf.exceptions.InvalidPatchSet): pyhf.PatchSet(patchsetspec) @@ -97,20 +100,31 @@ def test_patchset_repr(patchset): def test_patchset_verify(datadir): - patchset = pyhf.PatchSet(json.load(open(datadir.joinpath('example_patchset.json')))) - ws = pyhf.Workspace(json.load(open(datadir.joinpath('example_bkgonly.json')))) + with open( + datadir.joinpath("example_patchset.json"), encoding="utf-8" + ) as patch_file: + patchset = pyhf.PatchSet(json.load(patch_file)) + with open(datadir.joinpath("example_bkgonly.json"), encoding="utf-8") as ws_file: + ws = pyhf.Workspace(json.load(ws_file)) assert patchset.verify(ws) is None def test_patchset_verify_failure(datadir): - patchset = pyhf.PatchSet(json.load(open(datadir.joinpath('example_patchset.json')))) + with open( + datadir.joinpath("example_patchset.json"), encoding="utf-8" + ) as patch_file: + patchset = pyhf.PatchSet(json.load(patch_file)) with pytest.raises(pyhf.exceptions.PatchSetVerificationError): assert patchset.verify({}) def test_patchset_apply(datadir): - patchset = pyhf.PatchSet(json.load(open(datadir.joinpath('example_patchset.json')))) - ws = pyhf.Workspace(json.load(open(datadir.joinpath('example_bkgonly.json')))) + with open( + datadir.joinpath("example_patchset.json"), encoding="utf-8" + ) as patch_file: + patchset = pyhf.PatchSet(json.load(patch_file)) + with open(datadir.joinpath("example_bkgonly.json"), encoding="utf-8") as ws_file: + ws = pyhf.Workspace(json.load(ws_file)) with mock.patch('pyhf.patchset.PatchSet.verify') as m: assert m.call_count == 0 assert patchset.apply(ws, 'patch_channel1_signal_syst1') @@ -134,9 +148,10 @@ def test_patch_equality(patch): def test_patchset_get_string_values(datadir): - patchset = pyhf.PatchSet( - json.load(open(datadir.joinpath('patchset_good_stringvalues.json'))) - ) + with open( + datadir.joinpath('patchset_good_stringvalues.json'), encoding="utf-8" + ) as patch_file: + patchset = pyhf.PatchSet(json.load(patch_file)) assert patchset["Gtt_2100_5000_800"] assert patchset["Gbb_2200_5000_800"] assert patchset[[2100, 800, "Gtt"]] diff --git a/tests/test_pdf.py b/tests/test_pdf.py index 2538b708a2..948aa1c0f2 100644 --- a/tests/test_pdf.py +++ b/tests/test_pdf.py @@ -354,7 +354,10 @@ def test_pdf_integration_shapesys_zeros(backend): @pytest.mark.only_numpy def test_pdf_integration_histosys(backend): - source = json.load(open('validation/data/2bin_histosys_example2.json')) + with open( + "validation/data/2bin_histosys_example2.json", encoding="utf-8" + ) as spec_file: + source = json.load(spec_file) spec = { 'channels': [ { @@ -433,7 +436,10 @@ def test_pdf_integration_histosys(backend): def test_pdf_integration_normsys(backend): - source = json.load(open('validation/data/2bin_histosys_example2.json')) + with open( + "validation/data/2bin_histosys_example2.json", encoding="utf-8" + ) as spec_file: + source = json.load(spec_file) spec = { 'channels': [ { @@ -494,7 +500,10 @@ def test_pdf_integration_normsys(backend): @pytest.mark.only_numpy def test_pdf_integration_shapesys(backend): - source = json.load(open('validation/data/2bin_histosys_example2.json')) + with open( + "validation/data/2bin_histosys_example2.json", encoding="utf-8" + ) as spec_file: + source = json.load(spec_file) spec = { 'channels': [ { @@ -617,7 +626,10 @@ def test_invalid_modifier_name_resuse(): def test_override_paramset_defaults(): - source = json.load(open('validation/data/2bin_histosys_example2.json')) + with open( + "validation/data/2bin_histosys_example2.json", encoding="utf-8" + ) as spec_file: + source = json.load(spec_file) spec = { 'channels': [ { @@ -650,7 +662,10 @@ def test_override_paramset_defaults(): def test_override_paramsets_incorrect_num_parameters(): - source = json.load(open('validation/data/2bin_histosys_example2.json')) + with open( + "validation/data/2bin_histosys_example2.json", encoding="utf-8" + ) as spec_file: + source = json.load(spec_file) spec = { 'channels': [ { diff --git a/tests/test_public_api_repr.py b/tests/test_public_api_repr.py index a9245e7658..f1482d8921 100644 --- a/tests/test_public_api_repr.py +++ b/tests/test_public_api_repr.py @@ -125,7 +125,15 @@ def test_infer_calculators_public_api(): def test_infer_intervals_public_api(): - assert dir(pyhf.infer.intervals) == ["upperlimit"] + assert dir(pyhf.infer.intervals) == ["upper_limits.upper_limit"] + + +def test_infer_intervals_upper_limit_public_api(): + assert dir(pyhf.infer.intervals.upper_limits) == [ + "linear_grid_scan", + "toms748_scan", + "upper_limit", + ] def test_infer_mle_public_api(): diff --git a/tests/test_regression.py b/tests/test_regression.py index 883d1240f5..a6396a0b11 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -1,3 +1,5 @@ +import warnings + import numpy as np import pytest from skhep_testdata import data_path @@ -176,3 +178,19 @@ def test_sbottom_regionC_1600_850_60(get_json_from_tarfile): rtol=1e-5, ) ) + + +def test_deprecated_api_warning(): + with warnings.catch_warnings(record=True) as _warning: + # Cause all warnings to always be triggered + warnings.simplefilter("always") + + pyhf.exceptions._deprecated_api_warning( + "deprecated_api", "new_api", "0.9.9", "1.0.0" + ) + assert len(_warning) == 1 + assert issubclass(_warning[-1].category, DeprecationWarning) + assert ( + "deprecated_api is deprecated in favor of new_api as of pyhf v0.9.9 and will be removed in pyhf v1.0.0." + in str(_warning[-1].message) + ) diff --git a/tests/test_schema.py b/tests/test_schema.py index 965ab6bd1f..384fcf0276 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -49,14 +49,18 @@ def test_schema_changeable(datadir, monkeypatch, self_restoring_schema_globals): new_path = datadir / 'customschema' with pytest.raises(pyhf.exceptions.SchemaNotFound): - pyhf.Workspace(json.load(open(datadir / 'customschema' / 'custom.json'))) + with open( + datadir / "customschema" / "custom.json", encoding="utf-8" + ) as spec_file: + pyhf.Workspace(json.load(spec_file)) pyhf.schema(new_path) assert old_path != pyhf.schema.path assert new_path == pyhf.schema.path assert pyhf.schema.variables.SCHEMA_CACHE is not old_cache assert len(pyhf.schema.variables.SCHEMA_CACHE) == 0 - assert pyhf.Workspace(json.load(open(new_path / 'custom.json'))) + with open(new_path / "custom.json", encoding="utf-8") as spec_file: + assert pyhf.Workspace(json.load(spec_file)) assert len(pyhf.schema.variables.SCHEMA_CACHE) == 1 @@ -73,7 +77,8 @@ def test_schema_changeable_context(datadir, monkeypatch, self_restoring_schema_g assert new_path == pyhf.schema.path assert pyhf.schema.variables.SCHEMA_CACHE is not old_cache assert len(pyhf.schema.variables.SCHEMA_CACHE) == 0 - assert pyhf.Workspace(json.load(open(new_path / 'custom.json'))) + with open(new_path / "custom.json", encoding="utf-8") as spec_file: + assert pyhf.Workspace(json.load(spec_file)) assert len(pyhf.schema.variables.SCHEMA_CACHE) == 1 assert old_path == pyhf.schema.path assert old_cache == pyhf.schema.variables.SCHEMA_CACHE @@ -91,7 +96,8 @@ def test_schema_changeable_context_error( with pytest.raises(ZeroDivisionError): with pyhf.schema(new_path): # this populates the current cache - pyhf.Workspace(json.load(open(new_path / 'custom.json'))) + with open(new_path / "custom.json", encoding="utf-8") as spec_file: + pyhf.Workspace(json.load(spec_file)) raise ZeroDivisionError() assert old_path == pyhf.schema.path assert old_cache == pyhf.schema.variables.SCHEMA_CACHE @@ -569,7 +575,8 @@ def test_jsonpatch_fail(patch): @pytest.mark.parametrize('patchset_file', ['patchset_good.json']) def test_patchset(datadir, patchset_file): - patchset = json.load(open(datadir.joinpath(patchset_file))) + with open(datadir.joinpath(patchset_file), encoding="utf-8") as patch_file: + patchset = json.load(patch_file) pyhf.schema.validate(patchset, 'patchset.json') @@ -589,7 +596,8 @@ def test_patchset(datadir, patchset_file): ], ) def test_patchset_fail(datadir, patchset_file): - patchset = json.load(open(datadir.joinpath(patchset_file))) + with open(datadir.joinpath(patchset_file), encoding="utf-8") as patch_file: + patchset = json.load(patch_file) with pytest.raises(pyhf.exceptions.InvalidSpecification): pyhf.schema.validate(patchset, 'patchset.json') diff --git a/tests/test_scripts.py b/tests/test_scripts.py index 124dabafc6..56053dcf0a 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -16,9 +16,13 @@ @pytest.fixture(scope="function") def tarfile_path(tmpdir): - with open(tmpdir.join("test_file.txt").strpath, "w") as write_file: + with open( + tmpdir.join("test_file.txt").strpath, "w", encoding="utf-8" + ) as write_file: write_file.write("test file") - with tarfile.open(tmpdir.join("test_tar.tar.gz").strpath, mode="w:gz") as archive: + with tarfile.open( + tmpdir.join("test_tar.tar.gz").strpath, mode="w:gz", encoding="utf-8" + ) as archive: archive.add(tmpdir.join("test_file.txt").strpath) return Path(tmpdir.join("test_tar.tar.gz").strpath) @@ -702,9 +706,9 @@ def test_patchset_extract(datadir, tmpdir, script_runner, output_file, with_meta else: assert ( extracted_output - == json.load(datadir.joinpath("example_patchset.json").open())['patches'][ - 0 - ]['patch'] + == json.load( + datadir.joinpath("example_patchset.json").open(encoding="utf-8") + )["patches"][0]["patch"] ) diff --git a/tests/test_validation.py b/tests/test_validation.py index 7fb947ab1a..3c5de27318 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -10,7 +10,7 @@ @pytest.fixture(scope='module') def source_1bin_shapesys(): - with open('validation/data/1bin_example1.json') as read_json: + with open("validation/data/1bin_example1.json", encoding="utf-8") as read_json: return json.load(read_json) @@ -64,7 +64,7 @@ def expected_result_1bin_shapesys(): @pytest.fixture(scope='module') def source_1bin_shapesys_q0(): - with open('validation/data/1bin_example1_q0.json') as read_json: + with open("validation/data/1bin_example1_q0.json", encoding="utf-8") as read_json: return json.load(read_json) @@ -133,7 +133,7 @@ def expected_result_1bin_shapesys_q0_toys(): @pytest.fixture(scope='module') def source_1bin_lumi(): - with open('validation/data/1bin_lumi.json') as read_json: + with open("validation/data/1bin_lumi.json", encoding="utf-8") as read_json: return json.load(read_json) @@ -195,7 +195,7 @@ def expected_result_1bin_lumi(): @pytest.fixture(scope='module') def source_1bin_normsys(): - with open('validation/data/1bin_normsys.json') as read_json: + with open("validation/data/1bin_normsys.json", encoding="utf-8") as read_json: return json.load(read_json) @@ -249,7 +249,9 @@ def expected_result_1bin_normsys(): @pytest.fixture(scope='module') def source_2bin_histosys(): - with open('validation/data/2bin_histosys_example2.json') as read_json: + with open( + "validation/data/2bin_histosys_example2.json", encoding="utf-8" + ) as read_json: return json.load(read_json) @@ -306,7 +308,9 @@ def expected_result_2bin_histosys(): @pytest.fixture(scope='module') def source_2bin_2channel(): - with open('validation/data/2bin_2channel_example1.json') as read_json: + with open( + "validation/data/2bin_2channel_example1.json", encoding="utf-8" + ) as read_json: return json.load(read_json) @@ -380,7 +384,9 @@ def expected_result_2bin_2channel(): @pytest.fixture(scope='module') def source_2bin_2channel_couplednorm(): - with open('validation/data/2bin_2channel_couplednorm.json') as read_json: + with open( + "validation/data/2bin_2channel_couplednorm.json", encoding="utf-8" + ) as read_json: return json.load(read_json) @@ -465,7 +471,9 @@ def expected_result_2bin_2channel_couplednorm(): @pytest.fixture(scope='module') def source_2bin_2channel_coupledhistosys(): - with open('validation/data/2bin_2channel_coupledhisto.json') as read_json: + with open( + "validation/data/2bin_2channel_coupledhisto.json", encoding="utf-8" + ) as read_json: return json.load(read_json) @@ -567,7 +575,9 @@ def expected_result_2bin_2channel_coupledhistosys(): @pytest.fixture(scope='module') def source_2bin_2channel_coupledshapefactor(): - with open('validation/data/2bin_2channel_coupledshapefactor.json') as read_json: + with open( + "validation/data/2bin_2channel_coupledshapefactor.json", encoding="utf-8" + ) as read_json: return json.load(read_json) diff --git a/tests/test_workspace.py b/tests/test_workspace.py index aaa7a3b301..ccc54be422 100644 --- a/tests/test_workspace.py +++ b/tests/test_workspace.py @@ -889,7 +889,8 @@ def test_workspace_poiless(datadir): """ Test that a workspace with a measurement with empty POI string is treated as POI-less """ - spec = json.load(open(datadir.joinpath("poiless.json"))) + with open(datadir.joinpath("poiless.json"), encoding="utf-8") as spec_file: + spec = json.load(spec_file) ws = pyhf.Workspace(spec) model = ws.model() diff --git a/validation/makedata.py b/validation/makedata.py index 3e6e95ff06..5ae5424459 100644 --- a/validation/makedata.py +++ b/validation/makedata.py @@ -4,7 +4,8 @@ import ROOT if __name__ == "__main__": - source_data = json.load(open(sys.argv[1])) + with open(sys.argv[1], encoding="utf-8") as source_file: + source_data = json.load(source_file) root_file = sys.argv[2] binning = source_data["binning"] diff --git a/validation/manualonoff_roofit/onoff.py b/validation/manualonoff_roofit/onoff.py index 4708d1abad..ff31acacbe 100644 --- a/validation/manualonoff_roofit/onoff.py +++ b/validation/manualonoff_roofit/onoff.py @@ -1,7 +1,8 @@ import json import ROOT -d = json.load(open('data/source.json')) +with open("data/source.json", encoding="utf-8") as source_file: + d = json.load(source_file) nobs = d['bindata']['data'][0] b = d['bindata']['bkg'][0] deltab = d['bindata']['bkgerr'][0] diff --git a/validation/multichan_coupledhistosys_histfactory/makedata.py b/validation/multichan_coupledhistosys_histfactory/makedata.py index 50c7bb29f6..b09298329f 100644 --- a/validation/multichan_coupledhistosys_histfactory/makedata.py +++ b/validation/multichan_coupledhistosys_histfactory/makedata.py @@ -3,7 +3,8 @@ import json import sys -source_data = json.load(open(sys.argv[1])) +with open(sys.argv[1], encoding="utf-8") as source_file: + source_data = json.load(source_file) root_file = sys.argv[2] f = ROOT.TFile(root_file, 'RECREATE') diff --git a/validation/multichan_coupledoverall_histfactory/makedata.py b/validation/multichan_coupledoverall_histfactory/makedata.py index 0ae185fea2..0b33ff4f55 100644 --- a/validation/multichan_coupledoverall_histfactory/makedata.py +++ b/validation/multichan_coupledoverall_histfactory/makedata.py @@ -3,7 +3,8 @@ import json import sys -source_data = json.load(open(sys.argv[1])) +with open(sys.argv[1], encoding="utf-8") as source_file: + source_data = json.load(source_file) root_file = sys.argv[2] f = ROOT.TFile(root_file, 'RECREATE') diff --git a/validation/multichannel_histfactory/makedata.py b/validation/multichannel_histfactory/makedata.py index ac50787d63..cce668912d 100644 --- a/validation/multichannel_histfactory/makedata.py +++ b/validation/multichannel_histfactory/makedata.py @@ -3,7 +3,8 @@ import json import sys -source_data = json.load(open(sys.argv[1])) +with open(sys.argv[1], encoding="utf-8") as source_file: + source_data = json.load(source_file) root_file = sys.argv[2] f = ROOT.TFile(root_file, 'RECREATE') diff --git a/validation/run_toys.py b/validation/run_toys.py index ff541d4b2e..efa88bd4a6 100644 --- a/validation/run_toys.py +++ b/validation/run_toys.py @@ -53,7 +53,8 @@ def run_toys_ROOT(infile, ntoys): for idx in range(n_points) ] - json.dump(data, open("scan.json", "w")) + with open("scan.json", "w", encoding="utf-8") as write_file: + json.dump(data, write_file) canvas = ROOT.TCanvas() canvas.SetLogy(False) @@ -69,7 +70,7 @@ def run_toys_ROOT(infile, ntoys): def run_toys_pyhf(ntoys=2_000, seed=0): np.random.seed(seed) # with open("validation/xmlimport_input_bkg.json") as ws_json: - with open("debug/issue_workpace/issue_ws.json") as ws_json: + with open("debug/issue_workpace/issue_ws.json", encoding="utf-8") as ws_json: workspace = pyhf.Workspace(json.load(ws_json)) model = workspace.model() diff --git a/validation/standard_hypo_test_demo.py b/validation/standard_hypo_test_demo.py index c67f61b361..501004a27c 100644 --- a/validation/standard_hypo_test_demo.py +++ b/validation/standard_hypo_test_demo.py @@ -75,7 +75,7 @@ def standard_hypo_test_demo( def pyhf_version(ntoys=5000, seed=0): np.random.seed(seed) - with open("validation/xmlimport_input_bkg.json") as ws_json: + with open("validation/xmlimport_input_bkg.json", encoding="utf-8") as ws_json: workspace = pyhf.Workspace(json.load(ws_json)) model = workspace.model() diff --git a/validation/xmlimport_input2/makedata.py b/validation/xmlimport_input2/makedata.py index 50c7bb29f6..b09298329f 100644 --- a/validation/xmlimport_input2/makedata.py +++ b/validation/xmlimport_input2/makedata.py @@ -3,7 +3,8 @@ import json import sys -source_data = json.load(open(sys.argv[1])) +with open(sys.argv[1], encoding="utf-8") as source_file: + source_data = json.load(source_file) root_file = sys.argv[2] f = ROOT.TFile(root_file, 'RECREATE')