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() 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)