From cd560facbaecbbcc336e0693cde4807613701fa8 Mon Sep 17 00:00:00 2001 From: Remco de Boer Date: Mon, 4 Jan 2021 16:09:54 +0100 Subject: [PATCH] epic: export and import fit result (#159) * ci: add GitHub workflow for epic branches (#144) * ci: increase minimal coverage to 80% * feat: add CSVSummary callback (#173) * feat: add variable logging functionality using TensorFlow (#155) * feat: implement YAML optimize callback (#154) * feat: implement Loadable callback (#177) * feat: log execute time in optimize call (#156 and #164) * fix: copy initial parameters in optimize call (#174) * fix: implement temporary solution for #171 * fix: remove pytest color output VSCode * test: add additional resonance to fixture * refactor: change fit result dict structure * docs: use only 3 free parameters Speeds up CI and prevents memory problems on Read the Docs Co-authored-by: sjaeger Co-authored-by: spflueger --- .github/workflows/ci-epic.yml | 75 +++++++++ .github/workflows/pr-linting.yml | 2 +- .gitignore | 1 + codecov.yml | 2 +- cspell.json | 4 + docs/.gitignore | 1 + docs/usage/1_create_model.ipynb | 6 +- docs/usage/2_generate_data.ipynb | 6 +- docs/usage/3_perform_fit.ipynb | 198 +++++++++++++++++++----- examples/workflow.py | 8 +- reqs/3.6/requirements-dev.txt | 4 +- reqs/3.6/requirements-doc.txt | 4 +- reqs/3.6/requirements-sty.txt | 2 +- reqs/3.6/requirements-test.txt | 2 +- reqs/3.6/requirements.txt | 2 +- reqs/3.7/requirements-dev.txt | 4 +- reqs/3.7/requirements-doc.txt | 4 +- reqs/3.7/requirements-sty.txt | 2 +- reqs/3.7/requirements-test.txt | 2 +- reqs/3.7/requirements.txt | 2 +- reqs/3.8/requirements-dev.txt | 2 +- reqs/3.8/requirements-doc.txt | 2 +- setup.cfg | 3 + src/tensorwaves/optimizer/callbacks.py | 192 +++++++++++++++++++++++ src/tensorwaves/optimizer/minuit.py | 86 +++++----- tests/conftest.py | 99 +++++++++++- tests/data/test_generate.py | 47 +++--- tests/optimizer/__init__.py | 1 + tests/optimizer/test_callbacks.py | 23 +++ tests/optimizer/test_minuit.py | 30 ++++ tests/recipe/test_amplitude_creation.py | 37 +++-- tests/test_estimator.py | 22 +++ tox.ini | 2 +- 33 files changed, 728 insertions(+), 149 deletions(-) create mode 100644 .github/workflows/ci-epic.yml create mode 100644 src/tensorwaves/optimizer/callbacks.py create mode 100644 tests/optimizer/__init__.py create mode 100644 tests/optimizer/test_callbacks.py create mode 100644 tests/optimizer/test_minuit.py create mode 100644 tests/test_estimator.py diff --git a/.github/workflows/ci-epic.yml b/.github/workflows/ci-epic.yml new file mode 100644 index 00000000..83f92465 --- /dev/null +++ b/.github/workflows/ci-epic.yml @@ -0,0 +1,75 @@ +# cspell:ignore CUDA + +name: CI for epics + +on: + pull_request: + branches: + - epic/* + +jobs: + pytest: + name: Unit tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-20.04 + python-version: [3.6, 3.7, 3.8] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r reqs/${{ matrix.python-version }}/requirements-test.txt + pip install . + - name: Test with pytest + env: + CUDA_VISIBLE_DEVICES: "-1" + run: pytest -n auto + + documentation: + name: Build documentation + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r reqs/3.7/requirements-doc.txt + pip install tensorflow-cpu + pip install . + sudo apt-get -y install pandoc + - name: Build documentation + working-directory: docs + env: + CUDA_VISIBLE_DEVICES: "-1" + run: make html + - name: Test doctests in docstrings + working-directory: docs + run: make ignore-warnings=1 doctest + + style: + name: Style checks + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r reqs/3.7/requirements-sty.txt + pip install . + - name: Perform style checks + run: pre-commit run -a diff --git a/.github/workflows/pr-linting.yml b/.github/workflows/pr-linting.yml index b1a7d1c8..512e327a 100644 --- a/.github/workflows/pr-linting.yml +++ b/.github/workflows/pr-linting.yml @@ -19,7 +19,7 @@ jobs: - uses: docker://agilepathway/pull-request-label-checker:latest with: any_of: Bug,💡 Enhancement,📝 Docs,🔨 Maintenance,🖱️ DX - none_of: Epic,❌ Won't fix,💫 Good first issue + none_of: ❌ Won't fix,💫 Good first issue repo_token: ${{ secrets.GITHUB_TOKEN }} check-title: diff --git a/.gitignore b/.gitignore index 0d6fea5a..1d5baa53 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.pdf *.png *.svg +*.v2 *.xml *.yaml *.yml diff --git a/codecov.yml b/codecov.yml index 073e4f81..60c16b31 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,7 +10,7 @@ coverage: project: default: # basic - target: 75% # can't go below this percentage + target: 80% # can't go below this percentage threshold: 3% # allow drops by this percentage base: auto # advanced diff --git a/cspell.json b/cspell.json index ab833501..fcc84f1b 100644 --- a/cspell.json +++ b/cspell.json @@ -88,10 +88,12 @@ "spflueger", "struct", "sympy", + "tensorboard", "tensorflow", "tensorwaves", "toctree", "topness", + "traceback", "unbinned", "venv", "weisskopf", @@ -119,6 +121,7 @@ "genindex", "heli", "histtype", + "iloc", "iminuit", "indeterministic", "isort", @@ -128,6 +131,7 @@ "linestyle", "linkcheck", "linspace", + "logdir", "macos", "markdownlint", "mathrm", diff --git a/docs/.gitignore b/docs/.gitignore index b6c50e96..10288328 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,3 +1,4 @@ +*.csv *.doctree *.inv *build/ diff --git a/docs/usage/1_create_model.ipynb b/docs/usage/1_create_model.ipynb index 83e62786..749a7bfe 100644 --- a/docs/usage/1_create_model.ipynb +++ b/docs/usage/1_create_model.ipynb @@ -25,11 +25,7 @@ }, { "cell_type": "markdown", - "metadata": { - "jupyter": { - "source_hidden": true - } - }, + "metadata": {}, "source": [ "```{admonition} Simplified model: $J/\\psi \\to f_0\\gamma$\n", "---\n", diff --git a/docs/usage/2_generate_data.ipynb b/docs/usage/2_generate_data.ipynb index dab8a67a..7d5d53df 100644 --- a/docs/usage/2_generate_data.ipynb +++ b/docs/usage/2_generate_data.ipynb @@ -190,11 +190,7 @@ }, { "cell_type": "markdown", - "metadata": { - "jupyter": { - "source_hidden": true - } - }, + "metadata": {}, "source": [ "````{admonition} Available kinematic variables\n", "---\n", diff --git a/docs/usage/3_perform_fit.ipynb b/docs/usage/3_perform_fit.ipynb index 011c0d9f..188f585d 100644 --- a/docs/usage/3_perform_fit.ipynb +++ b/docs/usage/3_perform_fit.ipynb @@ -16,11 +16,7 @@ }, { "cell_type": "markdown", - "metadata": { - "jupyter": { - "source_hidden": true - } - }, + "metadata": {}, "source": [ "```{note}\n", "We first load the relevant data from the previous steps.\n", @@ -101,7 +97,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.3 Optimize the intensity model" + "## 3.2 Optimize the intensity model" ] }, { @@ -128,7 +124,7 @@ "source": [ "Starting the fit itself is quite simple: just create an optimizer instance of your choice, here [Minuit2](https://root.cern.ch/doc/master/Minuit2Page.html), and call its [optimize](tensorwaves.optimizer.minuit.Minuit2.optimize) method to start the fitting process. Notice that the [optimize](tensorwaves.optimizer.minuit.Minuit2.optimize) method requires a second argument. This is a mapping of parameter names that you want to fit to their initial values.\n", "\n", - "Let's first select a few of the parameters that we saw in [Step 3.1](#3.1-Define-estimator) and feed them to the optimizer to run the fit. Notice that we modify the parameters slightly to make the fit more interesting (we are running using a data sample that was generated with this very amplitude model after all)." + "Let's first select a few of the parameters that we saw in {ref}`Step 3.1 ` and feed them to the optimizer to run the fit. Notice that we modify the parameters slightly to make the fit more interesting (we are running using a data sample that was generated with this very amplitude model after all)." ] }, { @@ -139,9 +135,7 @@ "source": [ "initial_parameters = {\n", " \"Phase_J/psi(1S)_to_f(0)(1500)_0+gamma_1;f(0)(1500)_to_pi0_0+pi0_0;\": 0.0,\n", - " \"Width_f(0)(500)\": 0.1,\n", - " \"Position_f(0)(980)\": 0.9,\n", - " \"Position_f(0)(1500)\": 1.55,\n", + " \"Width_f(0)(500)\": 0.12,\n", " \"Position_f(0)(1710)\": 1.8,\n", " \"Width_f(0)(1710)\": 0.3,\n", "}" @@ -199,7 +193,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Finally, we create an [Optimizer](tensorwaves.interfaces.Optimizer) to [optimize](tensorwaves.optimizer.minuit.Minuit2.optimize) the model (which is embedded in the [Estimator](tensorwaves.interfaces.Estimator). Only the parameters that are given to the [optimize](tensorwaves.optimizer.minuit.Minuit2.optimize) method are optimized." + "Finally, we create an [Optimizer](tensorwaves.interfaces.Optimizer) to [optimize](tensorwaves.optimizer.minuit.Minuit2.optimize) the model (which is embedded in the [Estimator](tensorwaves.interfaces.Estimator). Only the parameters that are given to the [optimize](tensorwaves.optimizer.minuit.Minuit2.optimize) method are optimized.\n", + "\n", + "Here, we choose to use {class}`.Minuit2`, which is the most common optimizer choice in high-energy physics. Notice that the {class}`.Minuit2` class allows one to list {mod}`~tensorwaves.optimizer.callbacks`. These are called during the {meth}`~.Optimizer.optimize` method. Here, we use {class}`.CallbackList` to 'stack' several callbacks together." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{toggle}\n", + "To define your own callback, create a class that inherits from the {class}`~.Callback` class and feed it to the {class}`.Minuit2` constructor.\n", + "```" ] }, { @@ -208,20 +213,30 @@ "metadata": {}, "outputs": [], "source": [ + "from tensorwaves.optimizer.callbacks import (\n", + " CallbackList,\n", + " CSVSummary,\n", + " TFSummary,\n", + " YAMLSummary,\n", + ")\n", "from tensorwaves.optimizer.minuit import Minuit2\n", "\n", - "minuit2 = Minuit2()\n", + "minuit2 = Minuit2(\n", + " callback=CallbackList(\n", + " [\n", + " TFSummary(),\n", + " YAMLSummary(\"current_fit_result.yaml\"),\n", + " CSVSummary(\"fit_traceback.csv\", step_size=2),\n", + " ]\n", + " )\n", + ")\n", "result = minuit2.optimize(estimator, initial_parameters)\n", "result" ] }, { "cell_type": "markdown", - "metadata": { - "jupyter": { - "source_hidden": true - } - }, + "metadata": {}, "source": [ "```{admonition} Computation time ― around a minute in this case\n", "---\n", @@ -231,6 +246,85 @@ "```" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As can be seen, the values of the optimized parameters in the result are again comparable to the original values we saw in {ref}`usage/3_perform_fit:3.1 Define estimator`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "optimized_parameters = result[\"parameter_values\"]\n", + "optimized_parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3.3 Export and import" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In {ref}`usage/3_perform_fit:3.2 Optimize the intensity model`, we initialized {obj}`.Minuit2` with some callbacks that are {class}`.Loadable`. Such callback classes offer the possibility to {meth}`.Loadable.load_latest_parameters`, so you can pick up the optimize process in case it crashes or if you pause it. Loading the latest parameters goes as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "latest_parameters = YAMLSummary.load_latest_parameters(\"current_fit_result.yaml\")\n", + "latest_parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To restart the fit with the latest parameters, simply rerun as before." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "minuit2 = Minuit2()\n", + "minuit2.optimize(estimator, latest_parameters)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lo and behold: the parameters were already optimized, so the fit converged faster!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3.4 Visualize" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot optimized model" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -244,7 +338,7 @@ "metadata": {}, "outputs": [], "source": [ - "intensity.update_parameters(initial_parameters)\n", + "intensity.update_parameters(latest_parameters)\n", "compare_model(\"mSq_3_4\", data_set, phsp_set, intensity, np.sqrt)" ] }, @@ -252,30 +346,50 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.4 Export fit result" + "### Analyze optimization process" ] }, { "cell_type": "markdown", - "metadata": { - "jupyter": { - "source_hidden": true - } - }, + "metadata": {}, "source": [ - "```{admonition} Not implemented yet\n", - "---\n", - "class: dropdown\n", - "---\n", - "This functionality has not yet been implemented. See [issue 99](https://github.com/ComPWA/tensorwaves/issues/99).\n", - "```" + "Note that {ref}`in Step 3.2 `, we initialized {class}`.Minuit2` with a {class}`.TFSummary` callback as well. Its output files provide a nice, interactive representation of the fit process and can be viewed with [TensorBoard](https://www.tensorflow.org/tensorboard/get_started) as follows:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Optimizing an intensity model can take a long time, so it is important that you store the fit result in the end. We can simply [dump](json.dump) the file to [json](json):" + "````{tabbed} Terminal\n", + "```bash\n", + "tensorboard --logdir logs\n", + "```\n", + "````\n", + "\n", + "````{tabbed} Python\n", + "```python\n", + "import tensorboard as tb\n", + "\n", + "tb.notebook.list() # View open TensorBoard instances\n", + "tb.notebook.start(args_string=\"--logdir logs\")\n", + "```\n", + "See more info [here](https://www.tensorflow.org/tensorboard/tensorboard_in_notebooks#tensorboard_in_notebooks)\n", + "````\n", + "\n", + "````{tabbed} Jupyter notebook\n", + "```ipython\n", + "%load_ext tensorboard\n", + "%tensorboard --logdir logs\n", + "```\n", + "See more info [here](https://www.tensorflow.org/tensorboard/tensorboard_in_notebooks#tensorboard_in_notebooks)\n", + "````" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An alternative would be to use the output of the {class}`.CSVSummary` callback. Here's an example:" ] }, { @@ -284,17 +398,18 @@ "metadata": {}, "outputs": [], "source": [ - "import json\n", + "import pandas as pd\n", "\n", - "with open(\"tensorwaves_fit_result.json\", \"w\") as stream:\n", - " json.dump(result, stream, indent=2)" + "fit_traceback = pd.read_csv(\"fit_traceback.csv\")" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "...and [load](json.load) it back:" + "fit_traceback.plot(\"function_call\", \"estimator_value\");" ] }, { @@ -303,9 +418,14 @@ "metadata": {}, "outputs": [], "source": [ - "with open(\"tensorwaves_fit_result.json\") as stream:\n", - " result = json.load(stream)\n", - "result" + "fit_traceback.plot(\n", + " \"function_call\",\n", + " [\n", + " \"Phase_J/psi(1S)_to_f(0)(1500)_0+gamma_1;f(0)(1500)_to_pi0_0+pi0_0;\",\n", + " \"Position_f(0)(1710)\",\n", + " \"Width_f(0)(1710)\",\n", + " ],\n", + ");" ] } ], diff --git a/examples/workflow.py b/examples/workflow.py index 63cc46e2..591d798f 100644 --- a/examples/workflow.py +++ b/examples/workflow.py @@ -45,13 +45,13 @@ def perform_fit( estimator = UnbinnedNLL(intensity, dataset) free_params = { - # 'Mass_f2(1270):0': 1.3, + # 'Position_f2(1270):0': 1.3, "Width_f2(1270)": 0.3, - "Mass_f2(1950)": 1.9, + "Position_f2(1950)": 1.9, "Width_f2(1950)": 0.1, - # 'Mass_f0(980)': 0.8, + # 'Position_f0(980)': 0.8, "Width_f0(980)": 0.2, - "Mass_f0(1500)": 1.6, + "Position_f0(1500)": 1.6, "Width_f0(1500)": 0.01, # 'Magnitude_J/psi_to_f2(1270)_0+gamma_-1;f(2)(1270)_to_pi0_0+pi0_0;': , # 'Phase_J/psi_to_f2(1270)_0+gamma_-1;f(2)(1270)_to_pi0_0+pi0_0;': , diff --git a/reqs/3.6/requirements-dev.txt b/reqs/3.6/requirements-dev.txt index b67533dc..0ed52180 100644 --- a/reqs/3.6/requirements-dev.txt +++ b/reqs/3.6/requirements-dev.txt @@ -179,7 +179,7 @@ sphinx-copybutton==0.3.1 sphinx-panels==0.5.2 sphinx-thebe==0.0.8 sphinx-togglebutton==0.2.3 -sphinx==3.4.1 +sphinx==3.4.2 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==1.0.3 @@ -205,7 +205,7 @@ tox==3.20.1 tqdm==4.55.1 traitlets==4.3.3 typed-ast==1.4.2 -typing-extensions==3.7.4.3 +typing-extensions==3.7.4.3 ; python_version < "3.8.0" urllib3==1.26.2 virtualenv==20.2.2 wcwidth==0.2.5 diff --git a/reqs/3.6/requirements-doc.txt b/reqs/3.6/requirements-doc.txt index 5241e6de..0680bcd5 100644 --- a/reqs/3.6/requirements-doc.txt +++ b/reqs/3.6/requirements-doc.txt @@ -120,7 +120,7 @@ sphinx-copybutton==0.3.1 sphinx-panels==0.5.2 sphinx-thebe==0.0.8 sphinx-togglebutton==0.2.3 -sphinx==3.4.1 +sphinx==3.4.2 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==1.0.3 @@ -142,7 +142,7 @@ testpath==0.4.4 tornado==6.1 tqdm==4.55.1 traitlets==4.3.3 -typing-extensions==3.7.4.3 +typing-extensions==3.7.4.3 ; python_version < "3.8.0" urllib3==1.26.2 wcwidth==0.2.5 webencodings==0.5.1 diff --git a/reqs/3.6/requirements-sty.txt b/reqs/3.6/requirements-sty.txt index a3b74836..7756c6c5 100644 --- a/reqs/3.6/requirements-sty.txt +++ b/reqs/3.6/requirements-sty.txt @@ -116,7 +116,7 @@ toml==0.10.2 tqdm==4.55.1 traitlets==4.3.3 typed-ast==1.4.2 -typing-extensions==3.7.4.3 +typing-extensions==3.7.4.3 ; python_version < "3.8.0" urllib3==1.26.2 virtualenv==20.2.2 werkzeug==1.0.1 diff --git a/reqs/3.6/requirements-test.txt b/reqs/3.6/requirements-test.txt index 99a5f536..f8f42e8f 100644 --- a/reqs/3.6/requirements-test.txt +++ b/reqs/3.6/requirements-test.txt @@ -73,7 +73,7 @@ tensorflow==2.4.0 termcolor==1.1.0 toml==0.10.2 tqdm==4.55.1 -typing-extensions==3.7.4.3 +typing-extensions==3.7.4.3 ; python_version < "3.8.0" urllib3==1.26.2 werkzeug==1.0.1 wheel==0.36.2 diff --git a/reqs/3.6/requirements.txt b/reqs/3.6/requirements.txt index af7b7c8b..4556c1a1 100644 --- a/reqs/3.6/requirements.txt +++ b/reqs/3.6/requirements.txt @@ -57,7 +57,7 @@ tensorflow-probability==0.12.1 tensorflow==2.4.0 termcolor==1.1.0 tqdm==4.55.1 -typing-extensions==3.7.4.3 +typing-extensions==3.7.4.3 ; python_version < "3.8.0" urllib3==1.26.2 werkzeug==1.0.1 wheel==0.36.2 diff --git a/reqs/3.7/requirements-dev.txt b/reqs/3.7/requirements-dev.txt index 418d8671..292d0f72 100644 --- a/reqs/3.7/requirements-dev.txt +++ b/reqs/3.7/requirements-dev.txt @@ -175,7 +175,7 @@ sphinx-copybutton==0.3.1 sphinx-panels==0.5.2 sphinx-thebe==0.0.8 sphinx-togglebutton==0.2.3 -sphinx==3.4.1 +sphinx==3.4.2 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==1.0.3 @@ -201,7 +201,7 @@ tox==3.20.1 tqdm==4.55.1 traitlets==5.0.5 typed-ast==1.4.2 -typing-extensions==3.7.4.3 +typing-extensions==3.7.4.3 ; python_version < "3.8.0" urllib3==1.26.2 virtualenv==20.2.2 wcwidth==0.2.5 diff --git a/reqs/3.7/requirements-doc.txt b/reqs/3.7/requirements-doc.txt index 9eb20f32..b4c2a19e 100644 --- a/reqs/3.7/requirements-doc.txt +++ b/reqs/3.7/requirements-doc.txt @@ -119,7 +119,7 @@ sphinx-copybutton==0.3.1 sphinx-panels==0.5.2 sphinx-thebe==0.0.8 sphinx-togglebutton==0.2.3 -sphinx==3.4.1 +sphinx==3.4.2 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==1.0.3 @@ -141,7 +141,7 @@ testpath==0.4.4 tornado==6.1 tqdm==4.55.1 traitlets==5.0.5 -typing-extensions==3.7.4.3 +typing-extensions==3.7.4.3 ; python_version < "3.8.0" urllib3==1.26.2 wcwidth==0.2.5 webencodings==0.5.1 diff --git a/reqs/3.7/requirements-sty.txt b/reqs/3.7/requirements-sty.txt index 05fe6437..b2212f58 100644 --- a/reqs/3.7/requirements-sty.txt +++ b/reqs/3.7/requirements-sty.txt @@ -114,7 +114,7 @@ toml==0.10.2 tqdm==4.55.1 traitlets==5.0.5 typed-ast==1.4.2 -typing-extensions==3.7.4.3 +typing-extensions==3.7.4.3 ; python_version < "3.8.0" urllib3==1.26.2 virtualenv==20.2.2 werkzeug==1.0.1 diff --git a/reqs/3.7/requirements-test.txt b/reqs/3.7/requirements-test.txt index fa4366b3..ff81f953 100644 --- a/reqs/3.7/requirements-test.txt +++ b/reqs/3.7/requirements-test.txt @@ -72,7 +72,7 @@ tensorflow==2.4.0 termcolor==1.1.0 toml==0.10.2 tqdm==4.55.1 -typing-extensions==3.7.4.3 +typing-extensions==3.7.4.3 ; python_version < "3.8.0" urllib3==1.26.2 werkzeug==1.0.1 wheel==0.36.2 diff --git a/reqs/3.7/requirements.txt b/reqs/3.7/requirements.txt index 29d53ad8..8e0fea93 100644 --- a/reqs/3.7/requirements.txt +++ b/reqs/3.7/requirements.txt @@ -56,7 +56,7 @@ tensorflow-probability==0.12.1 tensorflow==2.4.0 termcolor==1.1.0 tqdm==4.55.1 -typing-extensions==3.7.4.3 +typing-extensions==3.7.4.3 ; python_version < "3.8.0" urllib3==1.26.2 werkzeug==1.0.1 wheel==0.36.2 diff --git a/reqs/3.8/requirements-dev.txt b/reqs/3.8/requirements-dev.txt index 4c862bfe..605d65f1 100644 --- a/reqs/3.8/requirements-dev.txt +++ b/reqs/3.8/requirements-dev.txt @@ -175,7 +175,7 @@ sphinx-copybutton==0.3.1 sphinx-panels==0.5.2 sphinx-thebe==0.0.8 sphinx-togglebutton==0.2.3 -sphinx==3.4.1 +sphinx==3.4.2 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==1.0.3 diff --git a/reqs/3.8/requirements-doc.txt b/reqs/3.8/requirements-doc.txt index 16259c1e..21aedc33 100644 --- a/reqs/3.8/requirements-doc.txt +++ b/reqs/3.8/requirements-doc.txt @@ -119,7 +119,7 @@ sphinx-copybutton==0.3.1 sphinx-panels==0.5.2 sphinx-thebe==0.0.8 sphinx-togglebutton==0.2.3 -sphinx==3.4.1 +sphinx==3.4.2 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==1.0.3 diff --git a/setup.cfg b/setup.cfg index c274999b..1f0f1e4e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,7 @@ install_requires = sympy tensorflow >= 2.0 tqdm + typing_extensions==3.7.4.3; python_version < "3.8.0" packages = find: package_dir = =src @@ -93,6 +94,8 @@ ignore_missing_imports = True ; External packages that miss stubs or type hints [mypy-amplitf.*] ignore_missing_imports = True +[mypy-iminuit.*] +ignore_missing_imports = True [mypy-phasespace.*] ignore_missing_imports = True [mypy-pydot.*] diff --git a/src/tensorwaves/optimizer/callbacks.py b/src/tensorwaves/optimizer/callbacks.py new file mode 100644 index 00000000..fc24485a --- /dev/null +++ b/src/tensorwaves/optimizer/callbacks.py @@ -0,0 +1,192 @@ +"""Collection of loggers that can be inserted into an optimizer as callback.""" + +from abc import ABC, abstractmethod +from datetime import datetime +from typing import IO, Any, Dict, Iterable, List, Optional + +import pandas as pd +import tensorflow as tf +import yaml + + +class Loadable(ABC): + @staticmethod + @abstractmethod + def load_latest_parameters(filename: str) -> dict: + pass + + +class Callback(ABC): + @abstractmethod + def on_iteration_end( + self, function_call: int, logs: Optional[Dict[str, Any]] = None + ) -> None: + pass + + @abstractmethod + def on_function_call_end(self) -> None: + pass + + +class CallbackList(Callback): + """Class for combining `Callback` s. + + Combine different `Callback` classes in to a chain as follows: + + >>> from tensorwaves.optimizer.callbacks import ( + ... CallbackList, TFSummary, YAMLSummary + ... ) + >>> from tensorwaves.optimizer.minuit import Minuit2 + >>> optimizer = Minuit2( + ... callback=CallbackList([TFSummary(), YAMLSummary("result.yml")]) + ... ) + """ + + def __init__(self, callbacks: Iterable[Callback]) -> None: + self.__callbacks: List[Callback] = list() + for callback in callbacks: + self.__callbacks.append(callback) + + def on_iteration_end( + self, function_call: int, logs: Optional[Dict[str, Any]] = None + ) -> None: + for callback in self.__callbacks: + callback.on_iteration_end(function_call, logs) + + def on_function_call_end(self) -> None: + for callback in self.__callbacks: + callback.on_function_call_end() + + +class CSVSummary(Callback, Loadable): + def __init__(self, filename: str, step_size: int = 10) -> None: + """Log fit parameters and the estimator value to a CSV file.""" + self.__step_size = step_size + self.__first_call = True + self.__stream = open(filename, "w") + _empty_file(self.__stream) + + def on_iteration_end( + self, function_call: int, logs: Optional[Dict[str, Any]] = None + ) -> None: + if logs is None: + return + if function_call % self.__step_size != 0: + return + output_dict = { + "function_call": function_call, + "time": logs["time"], + "estimator_type": logs["estimator"]["type"], + "estimator_value": logs["estimator"]["value"], + **logs["parameters"], + } + data_frame = pd.DataFrame(output_dict, index=[function_call]) + data_frame.to_csv( + self.__stream, + mode="a", + header=self.__first_call, + index=False, + ) + self.__first_call = False + + def on_function_call_end(self) -> None: + self.__stream.close() + + @staticmethod + def load_latest_parameters(filename: str) -> dict: + fit_traceback = pd.read_csv(filename) + parameter_traceback = fit_traceback[fit_traceback.columns[4:]] + parameter_names = parameter_traceback.columns + latest_parameter_values = parameter_traceback.iloc[-1] + return dict(zip(parameter_names, latest_parameter_values)) + + +class TFSummary(Callback): + def __init__( + self, + logdir: str = "logs", + step_size: int = 10, + subdir: Optional[str] = None, + ) -> None: + """Log fit parameters and the estimator value to a `tf.summary`. + + The logs can be viewed with `TensorBoard + `_ via: + + .. code-block:: bash + + tensorboard --logdir logs + """ + output_dir = logdir + "/" + datetime.now().strftime("%Y%m%d-%H%M%S") + if subdir is not None: + output_dir += "/" + subdir + self.__file_writer = tf.summary.create_file_writer(output_dir) + self.__file_writer.set_as_default() + self.__step_size = step_size + + def on_iteration_end( + self, function_call: int, logs: Optional[Dict[str, Any]] = None + ) -> None: + if logs is None: + return + if function_call % self.__step_size != 0: + return + parameters = logs["parameters"] + for par_name, value in parameters.items(): + tf.summary.scalar(par_name, value, step=function_call) + estimator_value = logs.get("estimator", {}).get("value", None) + if estimator_value is not None: + tf.summary.scalar("estimator", estimator_value, step=function_call) + self.__file_writer.flush() + + def on_function_call_end(self) -> None: + self.__file_writer.close() + + +class YAMLSummary(Callback, Loadable): + def __init__(self, filename: str, step_size: int = 10) -> None: + """Log fit parameters and the estimator value to a `tf.summary`. + + The logs can be viewed with `TensorBoard + `_ via: + + .. code-block:: bash + + tensorboard --logdir logs + """ + self.__step_size = step_size + self.__stream = open(filename, "w") + + def on_iteration_end( + self, function_call: int, logs: Optional[Dict[str, Any]] = None + ) -> None: + if function_call % self.__step_size != 0: + return + _empty_file(self.__stream) + yaml.dump( + logs, + self.__stream, + sort_keys=False, + Dumper=_IncreasedIndent, + default_flow_style=False, + ) + + def on_function_call_end(self) -> None: + self.__stream.close() + + @staticmethod + def load_latest_parameters(filename: str) -> dict: + with open(filename) as stream: + fit_stats = yaml.load(stream, Loader=yaml.SafeLoader) + return fit_stats["parameters"] + + +class _IncreasedIndent(yaml.Dumper): + # pylint: disable=too-many-ancestors + def increase_indent(self, flow=False, indentless=False): # type: ignore + return super().increase_indent(flow, False) + + +def _empty_file(stream: IO) -> None: + stream.seek(0) + stream.truncate() diff --git a/src/tensorwaves/optimizer/minuit.py b/src/tensorwaves/optimizer/minuit.py index 07ec3148..081e0706 100644 --- a/src/tensorwaves/optimizer/minuit.py +++ b/src/tensorwaves/optimizer/minuit.py @@ -1,12 +1,17 @@ """Minuit2 adapter to the `iminuit.Minuit` package.""" -import logging import time +from copy import deepcopy +from datetime import datetime +from typing import Dict, Optional -from iminuit import Minuit # type: ignore +from iminuit import Minuit +from tqdm import tqdm from tensorwaves.interfaces import Estimator, Optimizer +from .callbacks import Callback, CallbackList + class Minuit2(Optimizer): """The Minuit2 adapter. @@ -14,35 +19,42 @@ class Minuit2(Optimizer): Implements the `~.interfaces.Optimizer` interface. """ - def __init__(self) -> None: - pass - - def optimize(self, estimator: Estimator, initial_parameters: dict) -> dict: - parameters = initial_parameters + def __init__(self, callback: Optional[Callback] = None) -> None: + self.__callback: Callback = CallbackList([]) + if callback is not None: + self.__callback = callback - function_calls = 0 + def optimize( + self, estimator: Estimator, initial_parameters: Dict[str, float] + ) -> dict: + parameters = deepcopy(initial_parameters) + progress_bar = tqdm() + n_function_calls = 0 - def __func(pars: list) -> float: - """Wrap the estimator.""" + def wrapped_function(pars: list) -> float: + nonlocal n_function_calls + n_function_calls += 1 for i, k in enumerate(parameters.keys()): parameters[k] = pars[i] estimator.update_parameters(parameters) - nonlocal function_calls - function_calls += 1 - estimator_val = estimator() - if function_calls % 10 == 0: - logging.info( - "Function calls: %s\n" - "Current estimator value: %s\n" - "Parameters: %s", - function_calls, - estimator_val, - list(parameters.values()), - ) - return estimator_val + estimator_value = estimator() + progress_bar.set_postfix({"estimator": estimator_value}) + progress_bar.update() + logs = { + "time": datetime.now(), + "estimator": { + "type": self.__class__.__name__, + "value": float(estimator_value), + }, + "parameters": { + name: float(value) for name, value in parameters.items() + }, + } + self.__callback.on_iteration_end(n_function_calls, logs) + return estimator_value minuit = Minuit.from_array_func( - __func, + wrapped_function, list(parameters.values()), error=[0.1 * x if x != 0.0 else 0.1 for x in parameters.values()], name=list(parameters.keys()), @@ -53,19 +65,19 @@ def __func(pars: list) -> float: minuit.migrad() end_time = time.time() - par_states = minuit.get_param_states() - f_min = minuit.get_fmin() + self.__callback.on_function_call_end() - results: dict = {"params": {}} + parameter_values = dict() + parameter_errors = dict() for i, name in enumerate(parameters.keys()): - results["params"][name] = ( - par_states[i].value, - par_states[i].error, - ) + par_state = minuit.params[i] + parameter_values[name] = par_state.value + parameter_errors[name] = par_state.error - # return fit results - results["log_lh"] = f_min.fval - results["iterations"] = f_min.ncalls - results["func_calls"] = function_calls - results["time"] = end_time - start_time - return results + return { + "parameter_values": parameter_values, + "parameter_errors": parameter_errors, + "log_likelihood": minuit.fmin.fval, + "function_calls": minuit.fmin.ncalls, + "execution_time": end_time - start_time, + } diff --git a/tests/conftest.py b/tests/conftest.py index 6e81d593..6580f533 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,34 @@ +# pylint: disable=redefined-outer-name + +from copy import deepcopy + import expertsystem as es +import numpy as np import pytest from expertsystem.amplitude.model import AmplitudeModel from expertsystem.particle import ParticleCollection +from tensorwaves.data.generate import generate_data, generate_phsp +from tensorwaves.data.tf_phasespace import TFUniformRealNumberGenerator +from tensorwaves.estimator import UnbinnedNLL +from tensorwaves.optimizer.callbacks import ( + CallbackList, + CSVSummary, + YAMLSummary, +) +from tensorwaves.optimizer.minuit import Minuit2 +from tensorwaves.physics.helicity_formalism.amplitude import ( + IntensityBuilder, + IntensityTF, +) +from tensorwaves.physics.helicity_formalism.kinematics import ( + HelicityKinematics, +) + +N_PHSP_EVENTS = int(1e5) +N_DATA_EVENTS = int(1e4) +RNG = TFUniformRealNumberGenerator(seed=0) + @pytest.fixture(scope="session") def pdg() -> ParticleCollection: @@ -24,11 +50,82 @@ def canonical_model() -> AmplitudeModel: return __create_model(formalism="helicity") +@pytest.fixture(scope="session") +def kinematics(helicity_model: AmplitudeModel) -> HelicityKinematics: + return HelicityKinematics.from_model(helicity_model) + + +@pytest.fixture(scope="session") +def phsp_sample(kinematics: HelicityKinematics) -> np.ndarray: + return generate_phsp(N_PHSP_EVENTS, kinematics, random_generator=RNG) + + +@pytest.fixture(scope="session") +def intensity( + helicity_model: AmplitudeModel, + kinematics: HelicityKinematics, + phsp_sample: np.ndarray, +) -> IntensityTF: + # https://github.com/ComPWA/tensorwaves/issues/171 + model = deepcopy(helicity_model) + builder = IntensityBuilder(model.particles, kinematics, phsp_sample) + return builder.create_intensity(model) + + +@pytest.fixture(scope="session") +def data_sample( + kinematics: HelicityKinematics, + intensity: IntensityTF, +) -> np.ndarray: + return generate_data( + N_DATA_EVENTS, kinematics, intensity, random_generator=RNG + ) + + +@pytest.fixture(scope="session") +def data_set( + kinematics: HelicityKinematics, + data_sample: np.ndarray, +) -> dict: + return kinematics.convert(data_sample) + + +@pytest.fixture(scope="session") +def estimator(intensity: IntensityTF, data_set: dict) -> UnbinnedNLL: + return UnbinnedNLL(intensity, data_set) + + +@pytest.fixture(scope="session") +def free_parameters() -> dict: + return { + "Width_f(0)(500)": 0.3, + "Position_f(0)(980)": 1, + } + + +@pytest.fixture(scope="session") +def fit_result( + estimator: UnbinnedNLL, free_parameters: dict, output_dir: str +) -> dict: + optimizer = Minuit2( + callback=CallbackList( + [ + CSVSummary(filename=output_dir + "fit_traceback.csv"), + YAMLSummary(filename=output_dir + "fit_result.yml"), + ] + ) + ) + return optimizer.optimize(estimator, free_parameters) + + def __create_model(formalism: str) -> AmplitudeModel: result = es.generate_transitions( initial_state=("J/psi(1S)", [-1, +1]), final_state=["gamma", "pi0", "pi0"], - allowed_intermediate_particles=["f(0)(980)"], + allowed_intermediate_particles=[ + "f(0)(500)", + "f(0)(980)", + ], formalism_type=formalism, topology_building="isobar", allowed_interaction_types=["EM", "strong"], diff --git a/tests/data/test_generate.py b/tests/data/test_generate.py index da50c62f..fa4efacb 100644 --- a/tests/data/test_generate.py +++ b/tests/data/test_generate.py @@ -1,44 +1,37 @@ +import numpy as np import pytest -import tensorflow_probability as tfp -from expertsystem.amplitude.model import AmplitudeModel -from tensorwaves.data.generate import generate_data, generate_phsp +from tensorwaves.data.generate import generate_phsp from tensorwaves.data.tf_phasespace import TFUniformRealNumberGenerator -from tensorwaves.physics.helicity_formalism.amplitude import IntensityBuilder from tensorwaves.physics.helicity_formalism.kinematics import ( HelicityKinematics, ParticleReactionKinematicsInfo, ) -def test_generate_data(helicity_model: AmplitudeModel): - model = helicity_model - kinematics = HelicityKinematics.from_model(model) - seed_stream = tfp.util.SeedStream(seed=0, salt="") - rng_phsp = TFUniformRealNumberGenerator(seed=seed_stream()) - phsp_sample = generate_phsp(100, kinematics, random_generator=rng_phsp) - builder = IntensityBuilder(model.particles, kinematics, phsp_sample) - intensity = builder.create_intensity(model) - sample_size = 3 - rng_data = TFUniformRealNumberGenerator(seed=seed_stream()) - sample = generate_data( - sample_size, kinematics, intensity, random_generator=rng_data - ) - assert pytest.approx(sample) == [ +def test_generate_data(data_sample: np.ndarray): + sub_sample = data_sample[:, :5, :] + assert pytest.approx(sub_sample) == [ [ - [-0.08422478973, 0.37705970855, 1.33372169008, 1.38855370282], - [-0.74812541198, 0.21571030592, 1.14925355933, 1.38816652884], - [0.68504603202, 0.94438232899, 0.82637913006, 1.42970224729], + [-0.8479079437, -1.1974401527, -0.2752833345, 1.4928468490], + [0.3206982196, 0.1421767540, 1.4766674272, 1.5177642333], + [-0.9951263630, 0.9791290234, -0.1245358582, 1.4015988380], + [0.2008204816, 0.1575737069, -1.5131060421, 1.5344863094], + [0.5480920846, 1.1264596845, -0.7324093890, 1.4511167656], ], [ - [0.43799335002, -0.10877767209, -1.08273962958, 1.18076864737], - [0.81631935895, 0.09552881266, -1.06604036961, 1.35283548742], - [-0.78523290411, -0.49083296483, -0.38336947354, 1.01128561882], + [0.8678992735, 1.0149268626, 0.1697012666, 1.3529016749], + [-0.1907191335, 0.0871387639, -0.8534626992, 0.8891480571], + [0.1564453851, -0.6578253700, 0.3295845093, 0.7642342974], + [-0.0494885031, -0.1095653307, 0.6735347934, 0.6973675703], + [-0.6325919150, -1.0658346016, 0.7777590462, 1.4694569743], ], [ - [-0.35376856028, -0.26828203645, -0.25098206049, 0.52757764979], - [-0.06819394696, -0.31123911859, -0.08321318972, 0.35589798373], - [0.10018687208, -0.45354936415, -0.44300965651, 0.65591213388], + [-0.0199913298, 0.1825132900, 0.1055820678, 0.2511514760], + [-0.1299790860, -0.2293155180, -0.6232047280, 0.6899877094], + [0.8386809778, -0.3213036534, -0.2050486511, 0.9310668644], + [-0.1513319785, -0.0480083762, 0.8395712486, 0.8650461202], + [0.0844998304, -0.0606250829, -0.0453496571, 0.1763262600], ], ] diff --git a/tests/optimizer/__init__.py b/tests/optimizer/__init__.py new file mode 100644 index 00000000..948df262 --- /dev/null +++ b/tests/optimizer/__init__.py @@ -0,0 +1 @@ +"""Required to set mypy options for the tests folder.""" diff --git a/tests/optimizer/test_callbacks.py b/tests/optimizer/test_callbacks.py new file mode 100644 index 00000000..aadc3d8e --- /dev/null +++ b/tests/optimizer/test_callbacks.py @@ -0,0 +1,23 @@ +from typing import Type + +import pytest + +from tensorwaves.optimizer.callbacks import CSVSummary, Loadable, YAMLSummary + + +@pytest.mark.parametrize( + "callback_type, filename", + [ + (CSVSummary, "fit_traceback.csv"), + (YAMLSummary, "fit_result.yml"), + ], +) +def test_load_latest_parameters( + callback_type: Type[Loadable], + filename: str, + output_dir: str, + fit_result: dict, +): + expected = fit_result["parameter_values"] + imported = callback_type.load_latest_parameters(output_dir + filename) + assert pytest.approx(expected) == imported diff --git a/tests/optimizer/test_minuit.py b/tests/optimizer/test_minuit.py new file mode 100644 index 00000000..2b1f2de4 --- /dev/null +++ b/tests/optimizer/test_minuit.py @@ -0,0 +1,30 @@ +# pylint: disable=redefined-outer-name + +import pytest + + +class TestMinuit2: + @staticmethod + def test_optimize(fit_result: dict, free_parameters: dict): + result = fit_result + assert set(result) == { + "parameter_values", + "parameter_errors", + "log_likelihood", + "function_calls", + "execution_time", + } + par_values = result["parameter_values"] + par_errors = result["parameter_errors"] + assert set(par_values) == set(free_parameters) + assert pytest.approx(result["log_likelihood"]) == -13379.223862030514 + assert pytest.approx(par_values["Width_f(0)(500)"]) == 0.55868526502471 + assert pytest.approx(par_errors["Width_f(0)(500)"]) == 0.01057804923356 + assert ( + pytest.approx(par_values["Position_f(0)(980)"]) + == 0.990141023090767 + ) + assert ( + pytest.approx(par_errors["Position_f(0)(980)"]) + == 0.000721352674347 + ) diff --git a/tests/recipe/test_amplitude_creation.py b/tests/recipe/test_amplitude_creation.py index adb82df7..842b03b5 100644 --- a/tests/recipe/test_amplitude_creation.py +++ b/tests/recipe/test_amplitude_creation.py @@ -1,4 +1,5 @@ import os +from copy import deepcopy import expertsystem.amplitude.model as es @@ -21,7 +22,8 @@ def _generate_phsp(recipe: es.AmplitudeModel, number_of_events: int): def test_helicity(helicity_model: es.AmplitudeModel): - model = helicity_model + # https://github.com/ComPWA/tensorwaves/issues/171 + model = deepcopy(helicity_model) kinematics = HelicityKinematics.from_model(model) masses_is = kinematics.reaction_kinematics_info.initial_state_masses masses_fs = kinematics.reaction_kinematics_info.final_state_masses @@ -34,33 +36,44 @@ def test_helicity(helicity_model: es.AmplitudeModel): builder = IntensityBuilder(model.particles, kinematics, phsp_sample) intensity = builder.create_intensity(model) assert set(intensity.parameters) == { - "strength_incoherent", - "Position_J/psi(1S)", - "Width_J/psi(1S)", - "MesonRadius_J/psi(1S)", + "Magnitude_J/psi(1S)_to_f(0)(500)_0+gamma_1;f(0)(500)_to_pi0_0+pi0_0;", "Magnitude_J/psi(1S)_to_f(0)(980)_0+gamma_1;f(0)(980)_to_pi0_0+pi0_0;", + "MesonRadius_J/psi(1S)", + "MesonRadius_f(0)(500)", + "MesonRadius_f(0)(980)", + "Phase_J/psi(1S)_to_f(0)(500)_0+gamma_1;f(0)(500)_to_pi0_0+pi0_0;", "Phase_J/psi(1S)_to_f(0)(980)_0+gamma_1;f(0)(980)_to_pi0_0+pi0_0;", + "Position_J/psi(1S)", + "Position_f(0)(500)", "Position_f(0)(980)", + "Width_J/psi(1S)", + "Width_f(0)(500)", "Width_f(0)(980)", - "MesonRadius_f(0)(980)", + "strength_incoherent", } def test_canonical(canonical_model: es.AmplitudeModel): - model = canonical_model + # https://github.com/ComPWA/tensorwaves/issues/171 + model = deepcopy(canonical_model) particles = model.particles kinematics = HelicityKinematics.from_model(model) phsp_sample = _generate_phsp(model, NUMBER_OF_PHSP_EVENTS) builder = IntensityBuilder(particles, kinematics, phsp_sample) intensity = builder.create_intensity(model) assert set(intensity.parameters) == { - "strength_incoherent", - "Position_J/psi(1S)", - "Width_J/psi(1S)", - "MesonRadius_J/psi(1S)", + "Magnitude_J/psi(1S)_to_f(0)(500)_0+gamma_1;f(0)(500)_to_pi0_0+pi0_0;", "Magnitude_J/psi(1S)_to_f(0)(980)_0+gamma_1;f(0)(980)_to_pi0_0+pi0_0;", + "MesonRadius_J/psi(1S)", + "MesonRadius_f(0)(500)", + "MesonRadius_f(0)(980)", + "Phase_J/psi(1S)_to_f(0)(500)_0+gamma_1;f(0)(500)_to_pi0_0+pi0_0;", "Phase_J/psi(1S)_to_f(0)(980)_0+gamma_1;f(0)(980)_to_pi0_0+pi0_0;", + "Position_J/psi(1S)", + "Position_f(0)(500)", "Position_f(0)(980)", + "Width_J/psi(1S)", + "Width_f(0)(500)", "Width_f(0)(980)", - "MesonRadius_f(0)(980)", + "strength_incoherent", } diff --git a/tests/test_estimator.py b/tests/test_estimator.py new file mode 100644 index 00000000..b85af567 --- /dev/null +++ b/tests/test_estimator.py @@ -0,0 +1,22 @@ +from tensorwaves.estimator import UnbinnedNLL + + +class TestUnbinnedNLL: + @staticmethod + def test_parameters(estimator: UnbinnedNLL): + assert estimator.parameters == { + "strength_incoherent": 1.0, + "MesonRadius_J/psi(1S)": 1.0, + "MesonRadius_f(0)(500)": 1.0, + "MesonRadius_f(0)(980)": 1.0, + "Magnitude_J/psi(1S)_to_f(0)(500)_0+gamma_1;f(0)(500)_to_pi0_0+pi0_0;": 1.0, + "Phase_J/psi(1S)_to_f(0)(500)_0+gamma_1;f(0)(500)_to_pi0_0+pi0_0;": 0.0, + "Magnitude_J/psi(1S)_to_f(0)(980)_0+gamma_1;f(0)(980)_to_pi0_0+pi0_0;": 1.0, + "Phase_J/psi(1S)_to_f(0)(980)_0+gamma_1;f(0)(980)_to_pi0_0+pi0_0;": 0.0, + "Position_J/psi(1S)": 3.0969, + "Width_J/psi(1S)": 9.29e-05, + "Position_f(0)(500)": 0.475, + "Width_f(0)(500)": 0.55, + "Position_f(0)(980)": 0.99, + "Width_f(0)(980)": 0.06, + } diff --git a/tox.ini b/tox.ini index 11f52eb8..fbda4f69 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ allowlist_externals = pytest commands = pytest {posargs:tests} \ - --cov-fail-under=75 \ + --cov-fail-under=80 \ --cov-report=html \ --cov-report=xml \ --cov=tensorwaves