Repository: fusion-energy/openmc-plasma-source (language: python)

Issue:
 pep8 automatic formatting and relaxes matplotlib version requirement
Adds pep8 automatic formatting and relaxes matplotlib requirement a bit


Discussion / hints:

Fix patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index b2691e1..9ffc64f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -9,15 +9,14 @@ jobs:
       - run:
           name: install
           command: |
-            python setup.py install
+            pip install --upgrade pip
+            pip install .[tests]
             apt-get update -y && apt-get install -y curl
+
       - run:
           name: run tests
           command: |
-            pytest tests/test_point_source.py -v --cov=openmc_plasma_source --cov-append --cov-report term --cov-report xml
-            pytest tests/test_ring_source.py -v --cov=openmc_plasma_source --cov-append --cov-report term --cov-report xml
-            pytest tests/test_tokamak_source.py -v --cov=openmc_plasma_source --cov-append --cov-report term --cov-report xml
-            pytest tests/test_plotting.py -v --cov=openmc_plasma_source --cov-append --cov-report term --cov-report xml
+            pytest tests -v --cov=openmc_plasma_source --cov-append --cov-report term --cov-report xml
 
       - store_test_results:
           path: test-reports
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 776d907..b0c9f0a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,10 +20,16 @@ jobs:
         uses: actions/checkout@v2
       
       - name: Install
-        run: python setup.py install
+        run: |
+          python3 -m pip install --upgrade pip
+          python3 -m pip install .[tests]
+
+      - name: Run tests
+        run: |
+          python3 -m pytest -v tests
 
-      - name: run example
+      - name: Run examples
         run: |
-          python examples/point_source_example.py
-          python examples/ring_source_example.py
-          python examples/tokamak_source_example.py
+          python3 examples/point_source_example.py
+          python3 examples/ring_source_example.py
+          python3 examples/tokamak_source_example.py
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index 60809ea..b202720 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -19,29 +19,25 @@ jobs:
 
     steps:
     - uses: actions/checkout@v2
+    
     - name: Set up Python
       uses: actions/setup-python@v2
       with:
         python-version: '3.x'
-    - name: Autobump version
-      run: |
-        # from refs/tags/v1.2.3 get 1.2.3
-        VERSION=$(echo $GITHUB_REF | sed 's#.*/v##')
-        PLACEHOLDER='version="develop"'
-        VERSION_FILE='setup.py'
-        # Grep checks that the placeholder is in the file. If grep doesn't find
-        # the placeholder then it exits with exit code 1 and github actions fails.
-        grep "$PLACEHOLDER" "$VERSION_FILE"
-        sed -i "s/$PLACEHOLDER/version=\"${VERSION}\"/g" "$VERSION_FILE"
-      shell: bash
+    
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        pip install build
+        pip install setuptools build wheel twine
+    
     - name: Build package
       run: python -m build
+
+    - name: Check build
+      run: twine check dist/*
+
     - name: Publish package
-      uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
+      uses: pypa/gh-action-pypi-publish@release/v1
       with:
         user: __token__
         password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.gitignore b/.gitignore
index a449343..2cc66f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ share/python-wheels/
 .installed.cfg
 *.egg
 MANIFEST
+_version.py
 
 # PyInstaller
 #  Usually these files are written by a python script from a template
diff --git a/LICENSE b/LICENSE.txt
similarity index 100%
rename from LICENSE
rename to LICENSE.txt
diff --git a/openmc_plasma_source/__init__.py b/openmc_plasma_source/__init__.py
index 05fbcdb..e7553a0 100644
--- a/openmc_plasma_source/__init__.py
+++ b/openmc_plasma_source/__init__.py
@@ -1,3 +1,16 @@
+try:
+    from importlib.metadata import version, PackageNotFoundError
+except (ModuleNotFoundError, ImportError):
+    from importlib_metadata import version, PackageNotFoundError
+try:
+    __version__ = version("openmc_plasma_source")
+except PackageNotFoundError:
+    from setuptools_scm import get_version
+
+    __version__ = get_version(root="..", relative_to=__file__)
+
+__all__ = ["__version__"]
+
 from .tokamak_source import TokamakSource
 from .ring_source import FusionRingSource
 from .point_source import FusionPointSource
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..bc250d5
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,11 @@
+[build-system]
+requires = [
+    "setuptools >= 45",
+    "wheel",
+    "setuptools_scm[toml] >= 6.2",
+    "setuptools_scm_git_archive",
+]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+write_to = "openmc_plasma_source/_version.py"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 327be37..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-numpy >= 1.9
-matplotlib >= 3.4.3
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..a49a7c6
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,36 @@
+[metadata]
+name = openmc_plasma_source
+author = The openmc-plasma-source Development Team
+author_email = rdelaportemathurin@gmail.com
+description = Creates tokamak plasma sources for OpenMC
+long_description = file: README.md
+long_description_content_type = text/markdown
+url = https://github.com/fusion-energy/openmc-plasma-source
+license = MIT
+license_file = LICENSE.txt
+classifiers =
+    Natural Language :: English
+    Topic :: Scientific/Engineering
+    Programming Language :: Python :: 3
+    Programming Language :: Python :: 3.6
+    Programming Language :: Python :: 3.7
+    Programming Language :: Python :: 3.8
+    Programming Language :: Python :: 3.9
+    License :: OSI Approved :: MIT License
+    Operating System :: OS Independent
+project_urls =
+    Source = https://github.com/fusion-energy/openmc-plasma-source
+    Tracker = https://github.com/fusion-energy/openmc-plasma-source/issues
+
+[options]
+packages = find:
+python_requires= >=3.6
+install_requires=
+    numpy >= 1.9
+    matplotlib >= 3.4.3
+    importlib-metadata; python_version < "3.8"
+
+[options.extras_require]
+tests = 
+    pytest >= 5.4.3
+    hypothesis
diff --git a/setup.py b/setup.py
index fdc7383..1abbd06 100644
--- a/setup.py
+++ b/setup.py
@@ -1,32 +1,4 @@
 import setuptools
 
-with open("README.md", "r") as fh:
-    long_description = fh.read()
-
-setuptools.setup(
-    name="openmc_plasma_source",
-    version="develop",
-    author="The openmc-plasma-source Development Team",
-    author_email="rdelaportemathurin@gmail.com",
-    description="Creates tokamak plasma sources for OpenMC",
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    url="https://github.com/fusion-energy/openmc-plasma-source",
-    packages=setuptools.find_packages(),
-    classifiers=[
-        "Natural Language :: English",
-        "Topic :: Scientific/Engineering",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.6",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: OS Independent",
-    ],
-    tests_require=[
-        "pytest",
-    ],
-    python_requires=">=3.6",
-    install_requires=["numpy>=1.9", "matplotlib >= 3.2.2"],
-)
+if __name__ == "__main__":
+    setuptools.setup()


Test patch:
diff --git a/tests/test_tokamak_source.py b/tests/test_tokamak_source.py
index 957cc16..ca28b77 100644
--- a/tests/test_tokamak_source.py
+++ b/tests/test_tokamak_source.py
@@ -1,74 +1,118 @@
 from openmc_plasma_source import TokamakSource
-
 from openmc import Source
+import numpy as np
 
 import pytest
+from hypothesis import given, settings, assume, strategies as st
+
+
+@st.composite
+def tokamak_source_strategy(draw, return_dict=False):
+    """Defines a hypothesis strategy that automatically generates a TokamakSource.
+    When passed `return_dict=True`, only a dict of the args is passed.
+    Geometry attributes are varied, while plasma attributes are fixed.
+    """
+    # Used to avoid generation of inappropriate float values
+    finites = {"allow_nan": False, "allow_infinity": False, "allow_subnormal": False}
+
+    # Specify the base strategies for each geometry input
+    minor_radius = draw(st.floats(min_value=0.0, max_value=100.0, **finites))
+    major_radius = draw(st.floats(min_value=0.0, max_value=100.0, **finites))
+    pedestal_radius = draw(st.floats(min_value=0.0, max_value=100.0, **finites))
+    elongation = draw(st.floats(min_value=1.0, max_value=10.0, **finites))
+    triangularity = draw(st.floats(min_value=-1.0, max_value=1.0, **finites))
+    shafranov_factor = draw(st.floats(min_value=0.0, max_value=1.0, **finites))
+
+    # Specify requirements that must be satisfied for a valid tokamak
+    assume(major_radius > minor_radius)
+    assume(minor_radius > pedestal_radius)
+    assume(minor_radius > shafranov_factor)
+
+    args_dict = {
+        "elongation": elongation,
+        "triangularity": triangularity,
+        "major_radius": major_radius,
+        "minor_radius": minor_radius,
+        "pedestal_radius": pedestal_radius,
+        "shafranov_factor": shafranov_factor,
+        "ion_density_centre": 1.09e20,
+        "ion_density_peaking_factor": 1,
+        "ion_density_pedestal": 1.09e20,
+        "ion_density_separatrix": 3e19,
+        "ion_temperature_centre": 45.9,
+        "ion_temperature_peaking_factor": 8.06,
+        "ion_temperature_pedestal": 6.09,
+        "ion_temperature_separatrix": 0.1,
+        "mode": "H",
+        "ion_temperature_beta": 6,
+    }
+
+    return args_dict if return_dict else TokamakSource(**args_dict)
 
 
-def test_creation():
-    my_source = TokamakSource(
-        elongation=1.557,
-        ion_density_centre=1.09e20,
-        ion_density_peaking_factor=1,
-        ion_density_pedestal=1.09e20,
-        ion_density_separatrix=3e19,
-        ion_temperature_centre=45.9,
-        ion_temperature_peaking_factor=8.06,
-        ion_temperature_pedestal=6.09,
-        ion_temperature_separatrix=0.1,
-        major_radius=9.06,
-        minor_radius=2.92258,
-        pedestal_radius=0.8 * 2.92258,
-        mode="H",
-        shafranov_factor=0.44789,
-        triangularity=0.270,
-        ion_temperature_beta=6,
-    )
-    for source in my_source.make_openmc_sources():
+@given(tokamak_source=tokamak_source_strategy())
+@settings(max_examples=1, deadline=None)
+def test_creation(tokamak_source):
+    """Tests that the sources generated by TokamakSource are of type openmc.Source"""
+    for source in tokamak_source.sources:
         assert isinstance(source, Source)
 
 
-def test_strengths_are_normalised():
+@given(tokamak_source=tokamak_source_strategy())
+@settings(max_examples=100, deadline=None)
+def test_strengths_are_normalised(tokamak_source):
     """Tests that the sum of the strengths attribute is equal to"""
-    my_source = TokamakSource(
-        elongation=1.557,
-        ion_density_centre=1.09e20,
-        ion_density_peaking_factor=1,
-        ion_density_pedestal=1.09e20,
-        ion_density_separatrix=3e19,
-        ion_temperature_centre=45.9,
-        ion_temperature_peaking_factor=8.06,
-        ion_temperature_pedestal=6.09,
-        ion_temperature_separatrix=0.1,
-        major_radius=9.06,
-        minor_radius=2.92258,
-        pedestal_radius=0.8 * 2.92258,
-        mode="H",
-        shafranov_factor=0.44789,
-        triangularity=0.270,
-        ion_temperature_beta=6,
-    )
-    assert pytest.approx(sum(my_source.strengths), 1)
-
-
-def test_angles():
+    assert pytest.approx(sum(tokamak_source.strengths), 1)
+
+
+@given(tokamak_source=tokamak_source_strategy())
+@settings(max_examples=100, deadline=None)
+def test_source_locations_are_within_correct_range(tokamak_source):
+    """Tests that each source has RZ locations within the expected range.
+    As the function converting (a,alpha) coordinates to (R,Z) is not bijective, we
+    cannot convert back to validate each individual point. However, we can determine
+    whether the generated points are contained within the shell of the last closed
+    magnetic surface.
+    See "Tokamak D-T neutron source models for different plasma physics confinement
+    modes", C. Fausser et al., Fusion Engineering and Design, 2012 for more info.
+    """
+    R_0 = tokamak_source.major_radius
+    A = tokamak_source.minor_radius
+    El = tokamak_source.elongation
+    delta = tokamak_source.triangularity
+
+    def get_R_on_LCMS(alpha):
+        """Gets R on the last closed magnetic surface for a given alpha"""
+        return R_0 + A * np.cos(alpha + delta * np.sin(alpha))
+
+    approx_lt = lambda x, y: x < y or np.isclose(x, y)
+    approx_gt = lambda x, y: x > y or np.isclose(x, y)
+
+    for source in tokamak_source.sources:
+        R, Z = source.space.r.x[0], source.space.z.x[0]
+        # First test that the point is contained with a simple box with
+        # lower left (r_min,-z_max) and upper right (r_max,z_max)
+        assert approx_gt(R, R_0 - A)
+        assert approx_lt(R, R_0 + A)
+        assert approx_lt(abs(Z), A * El)
+        # For a given Z, we can determine the two values of alpha where
+        # where a = minor_radius, and from there determine the upper and
+        # lower bounds for R.
+        alpha_1 = np.arcsin(abs(Z) / (El * A))
+        alpha_2 = np.pi - alpha_1
+        R_max, R_min = get_R_on_LCMS(alpha_1), get_R_on_LCMS(alpha_2)
+        assert approx_lt(R_max, R_0 + A)
+        assert approx_gt(R_min, R_0 - A)
+        assert approx_lt(R, R_max)
+        assert approx_gt(R, R_min)
+
+
+@given(tokamak_args_dict=tokamak_source_strategy(return_dict=True))
+@settings(max_examples=1, deadline=None)
+def test_angles(tokamak_args_dict):
     """Checks that custom angles can be set"""
-    my_source = TokamakSource(
-        elongation=1.557,
-        ion_density_centre=1.09e20,
-        ion_density_peaking_factor=1,
-        ion_density_pedestal=1.09e20,
-        ion_density_separatrix=3e19,
-        ion_temperature_centre=45.9,
-        ion_temperature_peaking_factor=8.06,
-        ion_temperature_pedestal=6.09,
-        ion_temperature_separatrix=0.1,
-        major_radius=9.06,
-        minor_radius=2.92258,
-        pedestal_radius=0.8 * 2.92258,
-        mode="H",
-        shafranov_factor=0.44789,
-        triangularity=0.270,
-        ion_temperature_beta=6,
-        angles=(0, 1),
-    )
+    tokamak_args_dict["angles"] = (0, 1)
+    tokamak_source = TokamakSource(**tokamak_args_dict)
+    assert tokamak_source.angles == (0, 1)
+    for source in tokamak_source.sources:
+        assert (source.space.phi.a, source.space.phi.b) == (0, 1)